diff --git a/changelog.d/2266-error-list-show-more.fixed.md b/changelog.d/2266-error-list-show-more.fixed.md new file mode 100644 index 00000000..e88433a0 --- /dev/null +++ b/changelog.d/2266-error-list-show-more.fixed.md @@ -0,0 +1 @@ +Settings → Feedback's error list now offers **Show more** under the newest eight errors: the title has always reported the whole log (`Recent errors · 60`) while the list stopped at eight with nothing to open the rest, so the ninth error could not be read at all. Expanding draws every loaded error and folds back to **Show less**; a report of eight or fewer keeps the list and its title exactly as they were. diff --git a/core/client.js b/core/client.js index 2faa1cf2..56abb7a7 100644 --- a/core/client.js +++ b/core/client.js @@ -18677,6 +18677,7 @@ var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "Thi function FeedbackPage({ actions, run, Section, Row: Row4 }) { const [what, setWhat] = (0, import_react40.useState)(""); const [report, setReport] = (0, import_react40.useState)(null); + const [allErrors, setAllErrors] = (0, import_react40.useState)(false); const [preview, setPreview] = (0, import_react40.useState)(false); const [note, setNote] = (0, import_react40.useState)(""); const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { @@ -18718,7 +18719,7 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) { const browserErrors = report ? clientErrors().map((entry) => ({ ...entry, source: "browser" })) : []; const entries2 = [...report?.entries ?? [], ...browserErrors].sort((a, b) => new Date(b.time) - new Date(a.time)); const total = (report?.total ?? 0) + browserErrors.length; - const shown = entries2.slice(0, 8); + const shown = allErrors ? entries2 : entries2.slice(0, 8); return [ (0, import_react40.createElement)( Section, @@ -18768,6 +18769,16 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) { ), entry.code ? (0, import_react40.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null )), + entries2.length > 8 ? (0, import_react40.createElement)( + "div", + { key: "more", className: "bt-set-row bt-fb-more-row" }, + (0, import_react40.createElement)("button", { + type: "button", + className: "bt-soft bt-soft-sm bt-fb-more", + "aria-expanded": allErrors, + onClick: () => setAllErrors(!allErrors) + }, t(allErrors ? "Show less" : "Show more")) + ) : null, (0, import_react40.createElement)( Row4, { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, diff --git a/core/src/client/feedback.js b/core/src/client/feedback.js index 4f35cd26..a41ece19 100644 --- a/core/src/client/feedback.js +++ b/core/src/client/feedback.js @@ -42,6 +42,7 @@ const SOURCES = { turn: 'Model call', host: 'FlyBot', api: 'FlyBot', browser: 'T export function FeedbackPage({ actions, run, Section, Row }) { const [what, setWhat] = useState('') const [report, setReport] = useState(null) + const [allErrors, setAllErrors] = useState(false) const [preview, setPreview] = useState(false) const [note, setNote] = useState('') // Each action reads the report again, so it holds errors from the last few seconds. @@ -87,7 +88,9 @@ export function FeedbackPage({ actions, run, Section, Row }) { const browserErrors = report ? clientErrors().map(entry => ({ ...entry, source: 'browser' })) : [] const entries = [...(report?.entries ?? []), ...browserErrors].sort((a, b) => new Date(b.time) - new Date(a.time)) const total = (report?.total ?? 0) + browserErrors.length - const shown = entries.slice(0, 8) + // The list opens with eight errors; anything past them waits behind one control, so a + // burst of failures does not push the download and clear buttons off the page. + const shown = allErrors ? entries : entries.slice(0, 8) return [ h(Section, { key: 'report', title: t('Report a problem') }, h('div', { className: 'bt-set-row bt-set-paste bt-fb-what' }, @@ -113,6 +116,13 @@ export function FeedbackPage({ actions, run, Section, Row }) { h('span', { className: 'bt-fb-text' }, entrySummary(entry)), h('span', { className: 'bt-set-hint' }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? 'FlyBot')].filter(Boolean).join(' · '))), entry.code ? h('span', { className: 'bt-tag bt-fb-code' }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null)), + entries.length > 8 + ? h('div', { key: 'more', className: 'bt-set-row bt-fb-more-row' }, + h('button', { + type: 'button', className: 'bt-soft bt-soft-sm bt-fb-more', 'aria-expanded': allErrors, + onClick: () => setAllErrors(!allErrors), + }, t(allErrors ? 'Show less' : 'Show more'))) + : null, h(Row, { label: t('Error log'), hint: t('Kept on this computer across restarts, up to about 1 MB.') }, h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: downloadLog }, t('Download')), h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: clearLog, disabled: !report || total === 0 }, t('Clear')))), diff --git a/core/test/feedback-errors-more.test.js b/core/test/feedback-errors-more.test.js new file mode 100644 index 00000000..b16fafd2 --- /dev/null +++ b/core/test/feedback-errors-more.test.js @@ -0,0 +1,203 @@ +// #2266: the Feedback page's error list reported the whole count in its title +// (`Recent errors · 60`) while drawing eight rows and offering nothing to reach the rest, +// so a person reading a burst of failures could not see the ninth error at all. The list +// now draws the first eight and a `Show more` control that expands it to every loaded row, +// with `Show less` folding it back; a report of eight or fewer errors keeps the list and +// the title exactly as they were, and the control is not drawn. +// +// This test runs the real hooks: `useState` below re-renders the page, `useEffect` runs the +// report load, and a click is the control's own `onClick`. A `useEffect() {}` stub would +// never show the list filling in. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { FeedbackPage } from './src/client/feedback.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) + +const h = (type, props, ...children) => ({ type, props: { ...props, children: children.flat(Infinity) } }) +const all = (node, out = []) => { + if (Array.isArray(node)) { for (const child of node) all(child, out); return out } + if (node && typeof node === 'object') { + out.push(node) + for (const child of [node.props?.children].flat(Infinity)) all(child, out) + } + return out +} + +// A tiny hooks runtime: state slots survive re-renders, setters mark the tree dirty, and +// effects run after the tree is committed (and only when their deps change). +function hooks() { + const states = [] + const effectSlots = [] + let cursor = 0 + let dirty = false + let pending = [] + const sameDeps = (a, b) => (a?.length === b?.length && (a ?? []).every((value, index) => value === b?.[index])) + return { + react: { + createElement: h, + Fragment: Symbol('Fragment'), + Component: class {}, + useState: (initial) => { + const slot = cursor++ + if (!(slot in states)) states[slot] = typeof initial === 'function' ? initial() : initial + const set = (value) => { + const next = typeof value === 'function' ? value(states[slot]) : value + if (next !== states[slot]) { states[slot] = next; dirty = true } + } + return [states[slot], set] + }, + useEffect: (fn, deps) => { + const slot = cursor++ + if (!effectSlots[slot] || !sameDeps(effectSlots[slot].deps, deps)) pending.push({ slot, fn, deps }) + }, + useLayoutEffect: (fn, deps) => { const slot = cursor++; if (!effectSlots[slot] || !sameDeps(effectSlots[slot].deps, deps)) pending.push({ slot, fn, deps }) }, + useMemo: (make) => make(), + useCallback: (fn) => fn, + useRef: (value) => ({ current: value }), + }, + begin() { cursor = 0; pending = [] }, + end() { + for (const { slot, fn, deps } of pending) { + effectSlots[slot]?.cleanup?.() + const cleanup = fn() + effectSlots[slot] = { deps, cleanup: typeof cleanup === 'function' ? cleanup : undefined } + } + pending = [] + }, + get dirty() { return dirty }, + clear() { dirty = false }, + } +} + +const Section = ({ title, children }) => h('section', { className: 'bt-set-section' }, h('h3', null, title), h('div', { className: 'bt-set-card' }, children)) +const Row = ({ label, hint, children }) => h('div', { className: 'bt-set-row' }, + h('div', { className: 'bt-set-copy' }, h('span', null, label), hint ? h('span', { className: 'bt-set-hint' }, hint) : null), + children ? h('div', { className: 'bt-set-control' }, children) : null) + +const T0 = Date.UTC(2026, 10, 3, 4, 0, 0) +const errorEntries = count => Array.from({ length: count }, (_, index) => ({ + time: T0 - index * 1000, + level: 'error', + text: `failure number ${index + 1}`, + source: 'host', +})) + +// The host read caps its payload at fifty entries while `total` counts the whole log, so a +// sixty-error log arrives as fifty rows and a total of sixty — the shape the probe showed. +async function mount({ entries, total } = {}) { + const runtime = hooks() + const report = { + entries, + total, + markdown: '# diagnostics', + issuesUrl: 'https://github.com/ThinkFlowLab/FlyBot/issues/new', + } + const actions = { + diagnostics: () => Promise.resolve(report), + diagnosticsLog: () => Promise.resolve(''), + diagnosticsClear: () => Promise.resolve(), + } + const context = { + module: { exports: {} }, exports: {}, + document: { addEventListener() {}, removeEventListener() {}, activeElement: null, querySelector: () => null }, + navigator: { language: 'en' }, + localStorage: { getItem: () => null, setItem() {} }, + window: { addEventListener() {}, removeEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }) }, + console, setTimeout, clearTimeout, Date, JSON, Promise, Symbol, + __flybot__: { slots: { entries: () => [], subscribe: () => () => {}, version: () => 0 }, call: () => {} }, + require: (name) => { + if (name === 'react') return runtime.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}`) + }, + } + context.module.exports = context.exports + runInNewContext(outputFiles[0].text, context) + const { FeedbackPage } = context.module.exports + + // The renderer's one job: call the function components (Section, Row) the page hands + // its children to, so the drawn tree is what the browser would get. + const expand = (node) => { + if (Array.isArray(node)) return node.map(expand) + if (!node || typeof node !== 'object') return node + const children = [node.props?.children].flat(Infinity).map(expand) + if (typeof node.type === 'function') return expand(node.type({ ...node.props, children })) + return { type: node.type, props: { ...node.props, children } } + } + let tree = null + const render = () => { + runtime.begin() + tree = expand(FeedbackPage({ actions, run: fn => fn, Section, Row })) + runtime.end() + } + const settle = async () => { + for (let step = 0; step < 20; step += 1) { + runtime.clear() + render() + await new Promise(resolve => setTimeout(resolve, 0)) + if (!runtime.dirty) return + } + throw new Error('the page never settled') + } + + await settle() + const find = predicate => all(tree).find(predicate) + const rows = () => all(tree).filter(node => node.props?.className === 'bt-set-row bt-fb-entry') + const more = () => find(node => typeof node.props?.className === 'string' && node.props.className.split(' ').includes('bt-fb-more')) + const text = (node) => [node?.props?.children].flat(Infinity).filter(child => typeof child === 'string' || typeof child === 'number').join('') + const deepText = (node) => all(node).map(part => text(part)).filter(Boolean).join(' ') + const heading = () => find(node => node.type === 'h3' && text(node).startsWith('Recent errors')) + return { + get rows() { return rows() }, + get more() { return more() }, + get heading() { return heading() }, + text, deepText, + title: () => text(heading()), + async click() { more().props.onClick({}); await settle() }, + } +} + +test('#2266: a sixty-error report draws eight rows and offers the rest', async () => { + const page = await mount({ entries: errorEntries(50), total: 60 }) + assert.equal(page.title(), 'Recent errors · 60', 'the title still reports the whole log') + assert.equal(page.rows.length, 8, 'the list still opens with eight rows') + assert.ok(page.more, 'the list offers a way to see the remaining errors') + assert.equal(page.more.props['aria-expanded'], false, 'the control says the rest is not drawn yet') + assert.equal(page.text(page.more).trim(), 'Show more') +}) + +test('#2266: Show more draws the errors past the eighth, and Show less folds them back', async () => { + const page = await mount({ entries: errorEntries(50), total: 60 }) + await page.click() + assert.equal(page.rows.length, 50, 'every loaded error is drawn after the click') + const texts = page.rows.map(row => page.deepText(row)) + assert.ok(texts.some(value => value.includes('failure number 9')), 'the ninth error is reachable') + assert.equal(page.more.props['aria-expanded'], true) + assert.equal(page.text(page.more).trim(), 'Show less') + await page.click() + assert.equal(page.rows.length, 8, 'Show less folds the list back to eight rows') + assert.equal(page.text(page.more).trim(), 'Show more') +}) + +test('#2266: eight or fewer errors draw no control and keep the title as it was', async () => { + const page = await mount({ entries: errorEntries(3), total: 3 }) + assert.equal(page.title(), 'Recent errors · 3') + assert.equal(page.rows.length, 3) + assert.equal(page.more, undefined, 'nothing to expand means no control') + + const exact = await mount({ entries: errorEntries(8), total: 8 }) + assert.equal(exact.title(), 'Recent errors · 8') + assert.equal(exact.rows.length, 8) + assert.equal(exact.more, undefined, 'exactly eight rows are all drawn, so no control') +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 9294c855..37814c78 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -17,7 +17,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl - Read-only banner: when another dsh holds the team (`roster.readOnly` a non-empty string), the sidebar draws the host's own sentence and, beside it, one **Reload** control in both shapes (wide and rail; the control stays outside the `role="status"` line so a live region remains text). It calls `actions.reloadTeam`, which re-reads the whole team (`state` without `since`, see `stateRequest` / `stateAnswerIsStale` in `sources.js`) so the banner and every refused control clear once the other holder let go, without a page reload that would drop what is in the composer. Tests: `core/test/sidebar-readonly-reload.test.js`, `core/test/client-reload-state.test.js`. - Chat context rows: a tool the plugin registers at install draws one `tool-addition` row, and `cells.js` keeps every name in `TEAM_TOOLS` out of the chat (`SILENT_REGISTRATION_TOOLS`, #1101) — the Project Assistant's `project_digest` joins them (#1712), so its 09:00 Session shows the digest, not its registration. - A Settings dialog takes focus when it opens (`useDialogFocus` in `core/src/client/overlay-hooks.js`; the `.bt-settings` dialog carries `tabIndex -1` and a ref) and gives it back to the still-connected opener when it closes; it is not a focus trap, so Tab can still leave the dialog for the page behind the scrim (#2251). - +- Settings → Feedback's error list (`feedback.js`) still titles the block with the whole log (`Recent errors · 60`) but lists only the newest eight, so when the report holds more rows one `Show more` control (`aria-expanded`) sits under them and draws every loaded error, folding back to `Show less`; a report of eight or fewer draws no control (#2266). ## Does not own diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index bf82ba29..a4a70edf 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -526,7 +526,7 @@ diff --git a/src/client/connectors.js b/src/client/connectors.js diff --git a/src/client/feedback.js b/src/client/feedback.js --- a/src/client/feedback.js +++ b/src/client/feedback.js -@@ -37,7 +37,7 @@ function entrySummary(entry) { +@@ -37,11 +37,12 @@ function entrySummary(entry) { return entry.text } @@ -535,7 +535,23 @@ diff --git a/src/client/feedback.js b/src/client/feedback.js export function FeedbackPage({ actions, run, Section, Row }) { const [what, setWhat] = useState('') -@@ -99,19 +99,19 @@ export function FeedbackPage({ actions, run, Section, Row }) { + const [report, setReport] = useState(null) ++ const [allErrors, setAllErrors] = useState(false) + const [preview, setPreview] = useState(false) + const [note, setNote] = useState('') + // Each action reads the report again, so it holds errors from the last few seconds. +@@ -87,7 +87,9 @@ export function FeedbackPage({ actions, run, Section, Row }) { + const browserErrors = report ? clientErrors().map(entry => ({ ...entry, source: 'browser' })) : [] + const entries = [...(report?.entries ?? []), ...browserErrors].sort((a, b) => new Date(b.time) - new Date(a.time)) + const total = (report?.total ?? 0) + browserErrors.length +- const shown = entries.slice(0, 8) ++ // The list opens with eight errors; anything past them waits behind one control, so a ++ // burst of failures does not push the download and clear buttons off the page. ++ const shown = allErrors ? entries : entries.slice(0, 8) + return [ + h(Section, { key: 'report', title: t('Report a problem') }, + h('div', { className: 'bt-set-row bt-set-paste bt-fb-what' }, +@@ -99,20 +99,27 @@ export function FeedbackPage({ actions, run, Section, Row }) { h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: () => setPreview(!preview), 'aria-expanded': preview }, preview ? t('Hide') : t('Show')), h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: copyReport }, t('Copy'))), preview ? h('div', { className: 'bt-set-row bt-fb-preview' }, h('pre', { tabIndex: 0, 'aria-label': t('Diagnostics') }, report?.markdown || t('Loading…'))) : null, @@ -556,8 +572,16 @@ diff --git a/src/client/feedback.js b/src/client/feedback.js - h('span', { className: 'bt-set-hint' }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? 'DS Bot')].filter(Boolean).join(' · '))), + h('span', { className: 'bt-set-hint' }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? 'FlyBot')].filter(Boolean).join(' · '))), entry.code ? h('span', { className: 'bt-tag bt-fb-code' }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null)), ++ entries.length > 8 ++ ? h('div', { key: 'more', className: 'bt-set-row bt-fb-more-row' }, ++ h('button', { ++ type: 'button', className: 'bt-soft bt-soft-sm bt-fb-more', 'aria-expanded': allErrors, ++ onClick: () => setAllErrors(!allErrors), ++ }, t(allErrors ? 'Show less' : 'Show more'))) ++ : null, h(Row, { label: t('Error log'), hint: t('Kept on this computer across restarts, up to about 1 MB.') }, h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: downloadLog }, t('Download')), + h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: clearLog, disabled: !report || total === 0 }, t('Clear')))), @@ -133,9 +133,10 @@ export function ErrorToast({ useUi, actions }) { return () => { clearTimeout(fade); clearTimeout(gone) } }, [toast?.token]) diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 81cb9b23..3fef421f 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -5029,7 +5029,7 @@ diff --git a/client.js b/client.js .bt-mem-skel{display:flex;flex-direction:column;gap:24px} .bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} .bt-mem-skel-block span{display:block;height:12px;border-radius:6px;background:linear-gradient(90deg,var(--bt-hover) 25%,var(--bt-active) 50%,var(--bt-hover) 75%);background-size:200% 100%;animation:bt-mem-skel 1.4s ease-in-out infinite} -@@ -2574,4596 +2574,9014 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} +@@ -2574,2935 +2574,7487 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} @media (max-width:640px){.bt-mem{width:100vw;height:100%;border-radius:0;border:0}.bt-mem-body{padding:18px 18px 20px}.bt-mem-foot{padding:8px 12px 12px}} @media (prefers-reduced-motion:reduce){.bt-mem,.bt-mem-scrim,.bt-mem-summary>*,.bt-mem-empty,.bt-mem-reply,.bt-mem-skel-block span,.bt-mem-entries,.bt-mem-send>*,.bt-mem-changes li svg{animation:none}} @@ -6060,121 +6060,7 @@ diff --git a/client.js b/client.js + ) + ) + ); - } --@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} --@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} --[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} --[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} --[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} --[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} --[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} --[data-composer-card]>[class*="_row"]{display:contents!important} --[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} --[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} --[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} --[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} --[data-composer-placeholder]{font-size:0!important;line-height:0!important} --[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} --/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ --[data-composer-card] button[class*="_add"]{display:none!important} --.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-plus:active{scale:.9} --.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} --.bt-plus[aria-expanded=true] svg{rotate:45deg} --.bt-plus-menu{min-width:180px} --[data-composer-card] [class*="_tools"]{gap:0!important} --[data-composer-card] [class*="_modes"]:empty{display:none!important} --[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} --[data-composer-card] [class*="_trailing"]{gap:8px} --[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} --[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} --[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} --@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} --[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} --[data-composer-card] button[class*="_primary"]:disabled{display:none!important} --[data-composer-dock]{display:none!important} --.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} --.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} --@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} --.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-voice:hover{opacity:.9} --.bt-mic:active,.bt-voice:active{scale:.9} --[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} --body[data-bt-room] .bt-voice{display:none!important} --body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} --.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} --[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} --.bt-q{order:1} --div:has(> nav [class*="_marks"]){display:none!important} --/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) -- takes its surface and its text from the one --bt-user pair: a skin that repaints -- the host's bubble surface alone would otherwise leave the text unreadable. */ --[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} --[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} --/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ --[class*="_userRow"] [class*="_actions"]{display:none!important} --[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} --[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} --[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} --[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} --[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} --[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} --[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} --.bt-q-inline{display:flex;margin:4px 0 6px} --[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} --[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} --.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} --.bt-q-dock{width:100%;display:flex;justify-content:center} --.bt-q-dock .bt-q{margin:0 auto 8px} --[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} --[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} --[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} --[class*="_toBottomSlot"]>button:active{scale:.9} --[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} --[class*="_toBottomSlot"]>button svg{display:none} --[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} --@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} --[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} --.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} --@keyframes bt-news-in{from{translate:0 8px;scale:.9}} --.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} --@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} --.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} --.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} --.bt-news-ico svg{width:16px;height:16px} --.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} --.bt-news-x svg{width:12px;height:12px} --.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} --@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} --.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} --.bt-open-new{display:flex;flex-direction:column} --.bt-open-new .bt-new-sep{margin:18px 0 0} --.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} --.bt-earlier{display:flex;flex-direction:column;gap:6px} --.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} --.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} --.bt-urow{display:flex;justify-content:flex-end} --.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} --.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} --.bt-ububble p{margin:0;line-height:22px} --.bt-uimages{display:block;font-size:12px;opacity:.7} --.bt-earlier .bt-event{margin:4px 0} --.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} --.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} --`; --function installStyles(text, tag) { -- const style = document.createElement("style"); -- style.dataset.dshBot = tag; -- style.textContent = text; -- document.head.appendChild(style); -- return style; ++} +function openPlugins(actions) { + actions.closeSettings(); + actions.selectPanel("plugins"); @@ -6410,22 +6296,7 @@ diff --git a/client.js b/client.js + hidden > 0 ? (0, import_react6.createElement)("button", { key: "more", type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules") }, t("{count} more not shown", { count: hidden })) : null + ) + ); - } --var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; --var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; --function sheetClassNames(sheet) { -- const names = []; -- const visit = (rules) => { -- for (const rule of rules) { -- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); -- if (rule.cssRules) visit(rule.cssRules); -- } -- }; -- try { -- visit(sheet.cssRules); -- } catch { -- } -- return names; ++} +function ScheduleSummary({ bot, roster, actions }) { + const view = useSchedules(actions); + if (view === null || view.readFailure !== void 0) return null; @@ -6437,35 +6308,14 @@ diff --git a/client.js b/client.js + tasks.length === 0 ? (0, import_react6.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")) : (0, import_react6.createElement)(SummaryLines, { tasks, actions }), + tasks.length === 0 ? (0, import_react6.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view.available === false ? t("Switch on Automation tasks on the Plugins page.") : view.someHidden ? t("No scheduled tasks you can see. Add one") : t("No scheduled tasks yet. Add one")) : null + ); - } --function compileFragmentSelectors(text, names) { -- const compiled = /* @__PURE__ */ new Map(); -- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { -- if (!compiled.has(fragment)) { -- const exact = []; -- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); -- } -- return compiled.get(fragment); -- }); ++} +function RoomSchedules({ room: room2, actions }) { + const view = useSchedules(actions); + if (view === null || view.readFailure !== void 0 || room2.origin === "face-to-face") return null; + const tasks = view.available ? view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id) : []; + if (tasks.length === 0) return null; + return (0, import_react6.createElement)("div", null, (0, import_react6.createElement)(SummaryLines, { tasks, actions })); - } --function installCompiledStyles(text, tag) { -- const style = installStyles("", tag); -- const cache = /* @__PURE__ */ new WeakMap(); -- const render = () => { -- const names = /* @__PURE__ */ new Set(); -- for (const sheet of document.styleSheets) { -- if (sheet.ownerNode?.dataset?.dshBot) continue; -- let list = cache.get(sheet); -- if (!list) { -- list = sheetClassNames(sheet); -- cache.set(sheet, list); ++} +var SCHEDULE_CSS = ` +.bt-task{gap:10px} +.bt-task-copy{flex-wrap:wrap;gap:8px} @@ -6534,8 +6384,7 @@ diff --git a/client.js b/client.js + } else if (index === count && ownRef.current) { + event.preventDefault(); + ownRef.current.focus(); - } -- for (const name of list) names.add(name); ++ } + }; + document.addEventListener("keydown", onKey, true); + return () => document.removeEventListener("keydown", onKey, true); @@ -6554,32 +6403,13 @@ diff --git a/client.js b/client.js + if (multi) { + setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); + return; - } -- const next = compileFragmentSelectors(text, names); -- if (style.textContent !== next) style.textContent = next; -- }; -- render(); -- let timer; -- const schedule = () => { -- clearTimeout(timer); -- timer = setTimeout(render, 120); -- }; -- const onLoad = (event) => { -- if (event.target instanceof HTMLLinkElement) schedule(); ++ } + onAnswer({ selected: [label] }); - }; -- const observer = new MutationObserver(schedule); -- observer.observe(document.head, { childList: true }); -- document.addEventListener("load", onLoad, true); -- return () => { -- observer.disconnect(); -- document.removeEventListener("load", onLoad, true); -- clearTimeout(timer); -- style.remove(); ++ }; + const submit = () => { + if (!canSubmit) return; + onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); - }; ++ }; + useOptionKeys(locked ? 0 : options.length, (index) => choose(options[index].label), ownRef); + const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + const field = ownAnswer && !locked ? (0, import_react7.createElement)("textarea", { @@ -6670,44 +6500,7 @@ diff --git a/client.js b/client.js + ) : null, + foot + ); - } -- --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); -- }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); -- }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); -- return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); -- }; -- }, [enabled]); ++} +function SettledQuestion({ question, answer }) { + const options = optionsOf(question); + const prompt = question?.question ?? ""; @@ -6739,32 +6532,7 @@ diff --git a/client.js b/client.js + ); + })) + ); - } --var FIDGETS = ["hop", "sway", "stretch", "look"]; --function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { -- const look = given ?? lookOf(bot); -- const ref = (0, import_react2.useRef)(null); -- const maskId = (0, import_react2.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; -- const [poked, setPoked] = (0, import_react2.useState)(false); -- useGaze(ref, gaze); -- const common = { -- ref, -- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", -- "data-shape": look.image ? "image" : shapeId(look.shape), -- "data-state": STATES.includes(state) ? state : "idle", -- "data-live": live ? "" : void 0, -- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, -- "data-gaze": gaze ? "" : void 0, -- "data-poke": pokeable ? "" : void 0, -- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, -- role: label ? "img" : void 0, -- "aria-label": label, -- "aria-hidden": label ? void 0 : true, -- onClick: pokeable ? () => setPoked(true) : void 0, -- onAnimationEnd: poked ? (event) => { -- if (/^bt-poke/.test(event.animationName)) setPoked(false); -- } : void 0 ++} +function QuestionCard({ matched }) { + const pending = matched; + const questions = pending.questions ?? []; @@ -6780,17 +6548,9 @@ diff --git a/client.js b/client.js + return; + } + pending.answer({ answers: next }); - }; -- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); -- const box = shapeOf(look.shape).size; -- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); ++ }; + return (0, import_react7.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); - } --function StarIcon({ size = 8 }) { -- return (0, import_react2.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) ++} +function PendingQuestion({ sessionId, question, actions, roster, inline }) { + const [answered, setAnswered] = (0, import_react7.useState)(null); + if (answered === question.id) return null; @@ -6808,10 +6568,8 @@ diff --git a/client.js b/client.js + { className: inline ? "bt-q-inline" : "bt-q-dock" }, + ...locked ? [(0, import_react7.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly))] : [], + (0, import_react7.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline, locked }) - ); - } --function AdminStar() { -- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); ++ ); ++} +function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { + const roster = useRoster((value) => value); + const question = roster.questions?.[sessionId]; @@ -6819,13 +6577,7 @@ diff --git a/client.js b/client.js + const loading = useSession?.(historyLoading) === true; + if (question === void 0 || question.callId && (drawn || loading)) return null; + return (0, import_react7.createElement)(PendingQuestion, { sessionId, question, actions, roster, inline: false }); - } --function MainStar() { -- return (0, import_react2.createElement)( -- "svg", -- { viewBox: "0 0 20 20", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) -- ); ++} +function AnsweredCell({ node }) { + const data = node.data ?? {}; + const questions = Array.isArray(data.questions) ? data.questions : []; @@ -6838,16 +6590,7 @@ diff --git a/client.js b/client.js + question, + answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } + }))); - } --function useSettled(state) { -- const prev = (0, import_react2.useRef)(state); -- const [settled, setSettled] = (0, import_react2.useState)(false); -- (0, import_react2.useEffect)(() => { -- const was = prev.current; -- prev.current = state; -- if (state !== "idle") { -- setSettled(false); -- return void 0; ++} + +// src/client/secret-card.js +var import_react8 = require("react"); @@ -6901,27 +6644,9 @@ diff --git a/client.js b/client.js + if (event.key !== "Enter" || event.nativeEvent.isComposing) return; + event.preventDefault(); + onEnter?.(); - } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; ++ } + }); - } --function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { -- const settled = useSettled(state); -- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); -- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, -- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), -- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, -- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, -- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, -- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null ++} +function ScopeSwitch({ value, onChange, bot }) { + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react8.createElement)( @@ -6936,15 +6661,8 @@ diff --git a/client.js b/client.js + className: "bt-secret-seg", + onClick: () => onChange(id) + }, text2)) - ); - } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; ++ ); ++} +function SecretHead({ bot, title, detail, name, onClose }) { + return (0, import_react8.createElement)( + "div", @@ -7048,9 +6766,7 @@ diff --git a/client.js b/client.js + if (request) return (0, import_react8.createElement)("div", { className: "bt-q-inline" }, (0, import_react8.createElement)(RequestView, { bot, request, roster, actions })); + if (done && /already saved/.test(contentText(content))) { + return (0, import_react8.createElement)("div", { className: "bt-event" }, (0, import_react8.createElement)("span", { className: "bt-secret-event" }, (0, import_react8.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); - } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; ++ } + return null; +} +function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { @@ -7098,33 +6814,7 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) + ) + ); - } --function RoomAvatar({ room: room2, roster, size = 36 }) { -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const people = members.length === 1 ? [members[0], null] : members; -- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); -- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); -- if (size <= 20 && people.length > 1) { -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, -- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), -- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null -- ); -- } -- const seats = clusterSeats(Math.min(people.length, 4), size); -- const overflow = people.length > 4 ? people.length - 3 : 0; -- const gap = size / 14; -- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-cluster", style: { width: size, height: size } }, -- seats.map(([x, y, s], index) => { -- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); -- const style = { left: x, top: y, width: s, height: s }; -- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); -- if (overflow > 0 && index === 3) { -- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); ++} +function ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react8.useState)(""); + const [replaced, setReplaced] = (0, import_react8.useState)(false); @@ -7135,10 +6825,7 @@ diff --git a/client.js b/client.js + if (ok) { + setValue(""); + setReplaced(true); - } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) ++ } + }); + }; + const edit = (next) => { @@ -7156,24 +6843,8 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) + ), + value !== "" && !long ? (0, import_react8.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react8.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react8.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null - ); - } -- --// src/client/icons.js --var import_react3 = require("react"); --function PlusIcon() { -- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); --} --function SearchIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); --} --function PeopleIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), -- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), -- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) ++ ); ++} +function SecretRow({ secret, roster, actions, run, open, onToggle }) { + const [confirm, setConfirm] = (0, import_react8.useState)(false); + (0, import_react8.useEffect)(() => { @@ -7215,17 +6886,8 @@ diff --git a/client.js b/client.js + ] : (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react8.createElement)(TrashIcon), t("Delete")) + ) + ) : null - ); - } --function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); --} --function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) ++ ); ++} +function AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react8.useState)(""); + const [purpose, setPurpose] = (0, import_react8.useState)(""); @@ -7253,14 +6915,8 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) + ) - ); - } --function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) -- ); ++ ); ++} +function SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react8.useState)(null); + const [adding, setAdding] = (0, import_react8.useState)(false); @@ -7284,13 +6940,7 @@ diff --git a/client.js b/client.js + ), + adding ? null : (0, import_react8.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) + ]; - } --function ShareIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) -- ); ++} +var SECRET_CSS = ` +.bt-secret-head{align-items:center;gap:10px} +.bt-secret-who{position:relative;flex:none;display:inline-flex} @@ -7409,20 +7059,10 @@ diff --git a/client.js b/client.js + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); + } + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; - } --function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++} +function facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; - } --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); --var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) -- ); ++} + +// src/client/held-reply.js +var import_react9 = require("react"); @@ -7442,41 +7082,7 @@ diff --git a/client.js b/client.js + render() { + return this.state.failed ? this.props.fallback : this.props.children; + } - }; --var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; --function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -- ); --} --var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); --var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); --var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); --var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); --var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); --var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); --var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); --var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); --var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); --var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); --var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); --var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); --var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); --var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); --var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); --function PinMarkIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( -- "g", -- { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) -- ) ++}; +function useInlineEntry(policy) { + const slots = registry(); + const [, setVersion] = (0, import_react9.useState)(0); @@ -7534,32 +7140,9 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react9.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react9.createElement)(custom.component, { held, decide, call: call3, actions })) : plain - ); - } --var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); --var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); --var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); --var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); --var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); --var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); --var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); --var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); --var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); --var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); --var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); --var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); --var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; - --// src/client/questions.js --var import_react4 = require("react"); --var keyLetter = (index) => String.fromCharCode(65 + index); --var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); --var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; --function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); -- pick2.current = onPick; -- (0, import_react4.useEffect)(() => { -- if (count === 0) return void 0; ++ ); ++} ++ +// src/client/turn-reasoning.js +function turnReasoning(steps, current) { + const members = steps.length ? steps : [current]; @@ -7642,25 +7225,12 @@ diff --git a/client.js b/client.js + 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(); ++ const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); + close(); - } - }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); ++ } ++ }; + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); + return () => { @@ -7668,19 +7238,7 @@ diff --git a/client.js b/client.js + window.removeEventListener("keydown", onKey, true); + }; + }, [open]); - } --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_react4.useState)(""); -- const [picked, setPicked] = (0, import_react4.useState)([]); -- const ownRef = (0, import_react4.useRef)(null); -- const titleId = (0, import_react4.useId)(); -- const options = optionsOf(question); -- const multi = question.multiSelect === true; -- const custom = own.trim(); -- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -- const choose = (label) => { -- if (multi) { -- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); ++} +function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react10.useState)(null); + const [place, setPlace] = (0, import_react10.useState)(null); @@ -7692,75 +7250,19 @@ diff --git a/client.js b/client.js + const open = (kind, event) => { + if (panel === kind) { + close(); - return; - } -- onAnswer({ selected: [label] }); ++ return; ++ } + const rect = event.currentTarget.getBoundingClientRect(); + const below = rect.bottom + 220 < window.innerHeight; + setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); + setPanel(kind); - }; -- const submit = () => { -- if (!canSubmit) return; -- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); ++ }; + const copy2 = () => { + void navigator.clipboard?.writeText(text2).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1e3); + }); - }; -- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { -- ref: ownRef, -- rows: 1, -- value: own, -- autoComplete: "off", -- spellCheck: false, -- className: "bt-q-input", -- "aria-label": t("Your own answer"), -- placeholder: t("Type your own answer"), -- "aria-keyshortcuts": ownLetter?.toLowerCase(), -- onChange: (event) => setOwn(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- submit(); -- } -- }) : null; -- const rows = options.map((option, optionIndex) => { -- const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( -- "button", -- { -- key: option.label, -- type: "button", -- className: "bt-q-opt", -- "aria-pressed": multi ? selected : void 0, -- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -- onClick: () => choose(option.label) -- }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( -- "span", -- { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null -- ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null -- ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); -- } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( ++ }; + const download2 = () => { + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); + const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); @@ -7778,25 +7280,7 @@ diff --git a/client.js b/client.js + sessionId ? (0, import_react10.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react10.createElement)(ReplyIcon)) : null, + (0, import_react10.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react10.createElement)(CheckIcon) : (0, import_react10.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react10.createElement)( - "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) -- ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); -- } -- return (0, import_react4.createElement)( -- "form", -- { -- className: raised ? "bt-q bt-q-raised" : "bt-q", -- "aria-labelledby": titleId, -- onSubmit: (event) => { -- event.preventDefault(); -- submit(); -- } -- }, -- (0, import_react4.createElement)( ++ "div", + { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + REACTIONS.map((emoji) => (0, import_react10.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { + close(); @@ -7804,16 +7288,7 @@ diff --git a/client.js b/client.js + } }, emoji)) + ), document.body) : null, + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react10.createElement)( - "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-text" }, -- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), -- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, -- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null -- ), -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) ++ "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) @@ -7852,15 +7327,7 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), + t("Thought it through"), + peek ? (0, import_react10.createElement)("span", { className: "bt-think-peek" }, peek) : null - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( -- "div", -- { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -- ) : null, -- foot ++ ), + (0, import_react10.createElement)("div", { className: "bt-think-text" }, text2) + ); +} @@ -7933,14 +7400,8 @@ diff --git a/client.js b/client.js + { className: "bt-held-reply", "data-state": held.status, "data-held": isApprovalWaiting(held.status) ? "" : void 0 }, + children, + closing ? (0, import_react10.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null - ); - } --function SettledQuestion({ question, answer }) { -- const options = optionsOf(question); -- const prompt = question?.question ?? ""; -- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -- if (chosen.length === 0) { -- return (0, import_react4.createElement)( ++ ); ++} +function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) { + if (!(texts2 ?? []).some((text2) => String(text2 ?? "").trim() !== "")) return null; + const lead = (0, import_react10.createElement)("span", { className: "bt-lead", title: name }, (0, import_react10.createElement)(PersonMark, { name })); @@ -7950,17 +7411,125 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("span", { className: "bt-group-name" }, name), + (0, import_react10.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead }) + ); -+} + } +-@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} +-@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} +-[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +-[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} +-[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} +-[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} +-[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} +-[data-composer-card]>[class*="_row"]{display:contents!important} +-[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} +-[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} +-[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} +-[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} +-[data-composer-placeholder]{font-size:0!important;line-height:0!important} +-[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} +-/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ +-[data-composer-card] button[class*="_add"]{display:none!important} +-.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-plus:active{scale:.9} +-.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} +-.bt-plus[aria-expanded=true] svg{rotate:45deg} +-.bt-plus-menu{min-width:180px} +-[data-composer-card] [class*="_tools"]{gap:0!important} +-[data-composer-card] [class*="_modes"]:empty{display:none!important} +-[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} +-[data-composer-card] [class*="_trailing"]{gap:8px} +-[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} +-[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} +-[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} +-@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} +-[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} +-[data-composer-card] button[class*="_primary"]:disabled{display:none!important} +-[data-composer-dock]{display:none!important} +-.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} +-.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} +-@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} +-.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-voice:hover{opacity:.9} +-.bt-mic:active,.bt-voice:active{scale:.9} +-[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} +-body[data-bt-room] .bt-voice{display:none!important} +-body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} +-.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} +-[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} +-.bt-q{order:1} +-div:has(> nav [class*="_marks"]){display:none!important} +-/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) +- takes its surface and its text from the one --bt-user pair: a skin that repaints +- the host's bubble surface alone would otherwise leave the text unreadable. */ +-[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} +-[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} +-/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ +-[class*="_userRow"] [class*="_actions"]{display:none!important} +-[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} +-[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} +-[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} +-[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} +-[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} +-[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} +-[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} +-.bt-q-inline{display:flex;margin:4px 0 6px} +-[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} +-[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} +-.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} +-.bt-q-dock{width:100%;display:flex;justify-content:center} +-.bt-q-dock .bt-q{margin:0 auto 8px} +-[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} +-[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} +-[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} +-[class*="_toBottomSlot"]>button:active{scale:.9} +-[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} +-[class*="_toBottomSlot"]>button svg{display:none} +-[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} +-@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} +-[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} +-.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-@keyframes bt-news-in{from{translate:0 8px;scale:.9}} +-.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} +-@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} +-.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} +-.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} +-.bt-news-ico svg{width:16px;height:16px} +-.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} +-.bt-news-x svg{width:12px;height:12px} +-.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} +-@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} +-.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} +-.bt-open-new{display:flex;flex-direction:column} +-.bt-open-new .bt-new-sep{margin:18px 0 0} +-.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} +-.bt-earlier{display:flex;flex-direction:column;gap:6px} +-.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} +-.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} +-.bt-urow{display:flex;justify-content:flex-end} +-.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} +-.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} +-.bt-ububble p{margin:0;line-height:22px} +-.bt-uimages{display:block;font-size:12px;opacity:.7} +-.bt-earlier .bt-event{margin:4px 0} +-.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} +-.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} +-`; +-function installStyles(text, tag) { +- const style = document.createElement("style"); +- style.dataset.dshBot = tag; +- style.textContent = text; +- document.head.appendChild(style); +- return style; +function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { + if (whole && bots.length === 1) { + return (0, import_react10.createElement)( - "div", -- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) ++ "div", + { className: "bt-event" }, + (0, import_react10.createElement)( + "button", @@ -7968,10 +7537,9 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("span", null, prefix), + (0, import_react10.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react10.createElement)("span", { className: "bt-event-name" }, bots[0].name) - ) - ); - } -- return (0, import_react4.createElement)( ++ ) ++ ); ++ } + const parts = []; + parts.push((0, import_react10.createElement)("span", { key: "p" }, prefix)); + bots.forEach((bot, index) => { @@ -8033,54 +7601,12 @@ diff --git a/client.js b/client.js + room2?.admin === bot.id ? (0, import_react10.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + ); + return (0, import_react10.createElement)( - "div", -- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -- const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( -- "div", -- { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) -- ); -- })) ++ "div", + { className: "bt-group-msg" }, + name, + (0, import_react10.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) - ); - } --function QuestionCard({ matched }) { -- const pending = matched; -- const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); -- const question = questions[index]; -- if (question === void 0) return null; -- const onAnswer = (answer) => { -- const next = [...answers, { id: question.id, ...answer }]; -- if (index + 1 < questions.length) { -- setAnswers(next); -- setIndex(index + 1); -- return; -- } -- pending.answer({ answers: next }); -- }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); --} --function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); -- if (answered === question.id) return null; -- const onAnswer = (answer) => { -- setAnswered(question.id); -- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); -- }; -- const onDismiss = () => { -- setAnswered(question.id); -- void actions.dismissQuestion(sessionId); -- }; -- return (0, import_react4.createElement)( ++ ); ++} +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -8139,9 +7665,7 @@ diff --git a/client.js b/client.js + const mark = (0, import_react10.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react10.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react10.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react10.createElement)( - "div", -- { 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: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react10.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react10.createElement)( @@ -8152,32 +7676,14 @@ diff --git a/client.js b/client.js + ), + String(words ?? "").trim() === "" ? null : (0, import_react10.createElement)(Bubbles, { texts: [words], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react10.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null - ); - } --function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const question = useRoster((value) => value.questions?.[sessionId]); -- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; -- const loading = useSession?.(historyLoading) === true; -- if (question === void 0 || question.callId && (drawn || loading)) return null; -- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); ++ ); ++} +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; + const bot = roster.byId[sessionId]?.name ?? t("your Bot"); + return (0, import_react10.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react10.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot }))); - } --function AnsweredCell({ node }) { -- const data = node.data ?? {}; -- const questions = Array.isArray(data.questions) ? data.questions : []; -- if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; -- } -- const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { -- key: question.id, -- question, -- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -- }))); ++} +function GlobeMark() { + return (0, import_react10.createElement)( + "svg", @@ -8185,60 +7691,7 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react10.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ); - } -- --// src/client/secret-card.js --var import_react5 = require("react"); --var MIN_VALUE = 4; --var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) --); --var LockIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) --); --var WarnIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) --); --var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); --var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); --var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); -- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); --}; --var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; --function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { -- return (0, import_react5.createElement)("input", { -- className: "bt-secret-input", -- type: MASK_BY_CSS ? "text" : "password", -- "data-masked": MASK_BY_CSS ? "" : void 0, -- value, -- autoFocus, -- autoComplete: MASK_BY_CSS ? "off" : "new-password", -- autoCorrect: "off", -- autoCapitalize: "off", -- spellCheck: false, -- "data-1p-ignore": "", -- "data-lpignore": "true", -- "data-bwignore": "", -- "data-form-type": "other", -- "aria-label": label, -- placeholder: t("Paste or type the key"), -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- onEnter?.(); ++} +function RemoteFiles({ files, plugin, botId }) { + const [busy, setBusy] = (0, import_react10.useState)(""); + const [error, setError] = (0, import_react10.useState)(""); @@ -8257,8 +7710,7 @@ diff --git a/client.js b/client.js + setError(String(reason?.message ?? reason)); + } finally { + setBusy(""); - } -- }); ++ } + }; + return (0, import_react10.createElement)( + "div", @@ -8270,30 +7722,15 @@ diff --git a/client.js b/client.js + )), + error ? (0, import_react10.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null + ); - } --function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( ++} +function GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_react10.createElement)( - "div", -- { 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)) ++ "div", + { className: "bt-event" }, + (0, import_react10.createElement)("span", null, prefix), + room2 ? (0, import_react10.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react10.createElement)("span", null, name) : null - ); - } --function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( ++ ); ++} +function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { + if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; + const self = roster.byId[sessionId]; @@ -8303,40 +7740,11 @@ diff --git a/client.js b/client.js + if (!target) return null; + const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); + return (0, import_react10.createElement)( - "div", -- { 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 ++ "div", + { className: "bt-event" }, + (0, import_react10.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react10.createElement)(MemoryIcon), label) - ); - } --function SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); -- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -- const run = (promise) => { -- setBusy(true); -- setError(""); -- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -- }; -- const cancel = () => run(actions.secretCancel(request.id)); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -- }; -- const allow = () => { -- if (!busy) void run(actions.secretAllow(request.id)); ++ ); ++} +function PartLine({ time }) { + return (0, import_react10.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react10.createElement)("span", null, dayLabel(time)) : null); +} @@ -8366,27 +7774,7 @@ diff --git a/client.js b/client.js + busy.current = false; + if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); + }); - }; -- const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { -- bot, -- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -- detail: request.purpose, -- name: request.name, -- onClose: busy ? void 0 : cancel -- }), -- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, -- filling ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-body" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), -- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -- ) : null, -- (0, import_react5.createElement)( ++ }; + (0, import_react10.useEffect)(() => { + alive.current = true; + load(void 0); @@ -8427,24 +7815,10 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, + (0, import_react10.createElement)( - "div", -- { 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 }) : "" ++ "div", + { ref: top, className: "bt-earlier-top" }, + view.loading ? (0, import_react10.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react10.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react10.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null - ), -- warn, -- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-foot" }, -- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), -- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) -- ) ++ ), + rows.map((row, index) => { + const key = rows.length - index; + switch (row.kind) { @@ -8474,31 +7848,20 @@ diff --git a/client.js b/client.js + } + }), + (0, import_react10.createElement)(PartLine, { time: view.startedAt }) - ); ++ ); } --function SettledSecret({ bot, request, roster }) { -- const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( -- "p", -- { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -- )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) -- ) -- ); -+function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -+ const roster = useRoster((value) => value); -+ const data = node.data; -+ const source = data.source !== null && typeof data.source === "object" ? data.source : {}; +-var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; +-var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; +-function sheetClassNames(sheet) { +- const names = []; +- const visit = (rules) => { +- for (const rule of rules) { +- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); +- if (rule.cssRules) visit(rule.cssRules); ++function 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); @@ -8529,7 +7892,11 @@ diff --git a/client.js b/client.js + if (source.remote?.handle) { + const message = (0, import_react10.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react10.createElement)(import_react10.Fragment, null, (0, import_react10.createElement)(FirstContact, { source, roster, sessionId }), message) : message; -+ } + } +- }; +- try { +- visit(sheet.cssRules); +- } catch { + if (source.kind === "bot") { + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react10.createElement)(EarlierParts, { sessionId, roster, actions, useUi }); + if (source.role === "room" || source.role === "kickoff") return null; @@ -8573,21 +7940,21 @@ diff --git a/client.js b/client.js + } + if (source.kind === "user-question-reply") return null; + return (0, import_react10.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -+ } + } +- return names; } --function 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 compileFragmentSelectors(text, names) { +- const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { +- if (!compiled.has(fragment)) { +- const exact = []; +- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); +- } +- return compiled.get(fragment); +function NewDivider() { + return (0, import_react10.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react10.createElement)("span", null, t("New"))); - } --function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -- const bot = roster.byId[sessionId]; -- if (!bot) return null; -- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); -- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); -- if (done && /already saved/.test(contentText(content))) { -- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -- } ++} +function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { + const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + (0, import_react10.useEffect)(() => { @@ -8603,22 +7970,25 @@ diff --git a/client.js b/client.js + const fresh = useTurnClock((state) => { + const own = state[sessionId]?.[turn]; + return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; -+ }); + }); + return fresh ? (0, import_react10.createElement)(NewDivider) : null; -+} -+function Nothing() { - return null; } --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 installCompiledStyles(text, tag) { +- const style = installStyles("", tag); +- const cache = /* @__PURE__ */ new WeakMap(); +- const render = () => { +- const names = /* @__PURE__ */ new Set(); +- for (const sheet of document.styleSheets) { +- if (sheet.ownerNode?.dataset?.dshBot) continue; +- let list = cache.get(sheet); +- if (!list) { +- list = sheetClassNames(sheet); +- cache.set(sheet, list); +- } +- for (const name of list) names.add(name); ++function Nothing() { ++ return 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([ @@ -8674,39 +8044,7 @@ diff --git a/client.js b/client.js + } + if (parts.length === 0) return null; + return (0, import_react10.createElement)("div", { className: "bt-event" }, parts.join(" · ")); - } --function ScopeEditor({ secret, roster, onSave }) { -- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); -- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); -- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, -- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( -- "label", -- { key: id, className: "bt-secret-radio" }, -- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), -- text -- )) -- ), -- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { -- key: bot.id, -- type: "button", -- className: "bt-secret-bot", -- "aria-pressed": picked.includes(bot.id), -- onClick: () => toggle(bot.id) -- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) -- ) -- ); ++} +var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +function clockLabel(time, now = Date.now()) { + const date = new Date(time); @@ -8717,36 +8055,7 @@ diff --git a/client.js b/client.js + 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 ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!long) return; -- void Promise.resolve(onSave(value)).then((ok) => { -- if (ok) { -- setValue(""); -- setReplaced(true); -- } -- }); -- }; -- const edit = (next) => { -- setValue(next); -- setReplaced(false); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -- ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null -- ); ++} +function nextTurnStart(turns, turn) { + let next; + for (const key in turns) { @@ -8754,48 +8063,7 @@ diff --git a/client.js b/client.js + if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; + } + return next === void 0 ? void 0 : turns[next].first; - } --function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { -- if (!open) setConfirm(false); -- }, [open]); -- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) -- ), -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-control" }, -- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -- ) -- ), -- open ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit" }, -- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), -- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- confirm ? [ -- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), -- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), -- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) -- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) -- ) -- ) : null ++} +function openingTurn(turns, firstTurns, hiddenTurns) { + if (!turns || !firstTurns) return null; + const start = nextTurnStart(turns, -Infinity); @@ -8837,62 +8105,11 @@ diff --git a/client.js b/client.js + null, + label ? (0, import_react10.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + fresh ? (0, import_react10.createElement)(NewDivider) : 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)(""); -- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!ready) return; -- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -- if (ok) { -- setValue(""); -- onDone(); -- } -- }); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) -- ) -- ); ++ ); ++} +function Shimmer({ children }) { + return (0, import_react10.createElement)("span", { className: "bt-shimmer" }, children); - } --function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); -- const secrets = roster.secrets ?? []; -- return [ -- (0, import_react5.createElement)( -- "p", -- { key: "about", className: "bt-secret-about" }, -- t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), -- t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), -- t(" before anything goes to a model.") -- ), -- (0, import_react5.createElement)( -- Section, -- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -- ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -- ]; ++} +var PHRASES = { + thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], + working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] @@ -8922,25 +8139,46 @@ diff --git a/client.js b/client.js + case "message_bot": { + const target = findBot(arg("to")); + return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -+ } -+ 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" }; + } +- const next = compileFragmentSelectors(text, names); +- if (style.textContent !== next) style.textContent = next; +- }; +- render(); +- let timer; +- const schedule = () => { +- clearTimeout(timer); +- timer = setTimeout(render, 120); +- }; +- const onLoad = (event) => { +- if (event.target instanceof HTMLLinkElement) schedule(); +- }; +- const observer = new MutationObserver(schedule); +- observer.observe(document.head, { childList: true }); +- document.addEventListener("load", onLoad, true); +- return () => { +- observer.disconnect(); +- document.removeEventListener("load", onLoad, true); +- clearTimeout(timer); +- style.remove(); +- }; ++ 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": @@ -8951,98 +8189,38 @@ diff --git a/client.js b/client.js + return { label: phrase ? t(phrase) : null, state: toolState(name) }; + } } --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/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"); +function workPhrase(tool) { + if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); - } --var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); --var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); --var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); -- --// src/client/cells.js --var import_react6 = require("react"); --var import_react_dom = require("react-dom"); --var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); --var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); --function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { -- const onClick = (event) => { -- const target = mentionTarget(event); -- if (target === null) return; -- event.preventDefault(); -- event.stopPropagation(); -- actions.openSession(target); -- }; -- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( ++} +function workText(tool, step) { + return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; +} @@ -9071,12 +8249,54 @@ diff --git a/client.js b/client.js + setLive({ replies: entry.replies ?? [] }); + } + timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); -+ }; + }; +- window.addEventListener("pointermove", onMove, { passive: true }); +- document.documentElement.addEventListener("pointerleave", onLeave); + void tick(); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- window.removeEventListener("pointermove", onMove); +- document.documentElement.removeEventListener("pointerleave", onLeave); + stop = true; + clearTimeout(timer); -+ }; + }; +- }, [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" }) + }, [done, sessionId, callId]); + let replies = live.replies; + if (done) { @@ -9089,17 +8309,7 @@ diff --git a/client.js b/client.js + if (replies.length === 0) return null; + const room2 = roster.roomsById[sessionId]; + return (0, import_react10.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-group-replies" }, + replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; @@ -9147,24 +8357,7 @@ diff --git a/client.js b/client.js + } + if (reply.kind === "approval" || reply.kind === "working") { + return (0, import_react10.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 -- ); ++ "div", + { key: `${reply.botId}-${index}`, className: "bt-event" }, + (0, import_react10.createElement)( + "button", @@ -9196,60 +8389,11 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react10.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); - }) ++ }) ); } --var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; --function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (refs.some((ref) => ref.current?.contains(event.target))) return; -- close(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -- } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [open]); --} --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); -- const close = () => setPanel(null); -- useDismiss(panel !== null, close, [tools, pop]); -- const open = (kind, event) => { -- if (panel === kind) { -- close(); -- return; -- } -- const rect = event.currentTarget.getBoundingClientRect(); -- const below = rect.bottom + 220 < window.innerHeight; -- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -- setPanel(kind); -- }; -- const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { -- setCopied(true); -- setTimeout(() => setCopied(false), 1e3); -- }); -- }; -- const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -- link.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); +-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 })); +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) { @@ -9261,41 +8405,15 @@ diff --git a/client.js b/client.js + seen.add(id); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); - }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -- close(); -- run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( -- "div", -- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, -- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -- close(); -- react(emoji); -- } }, emoji)) -- ), document.body) : null, -- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, -- item(t("Copy"), CopyIcon, copy2), -- item(t("Download"), DownloadIcon, download2) -- ), document.body) : null -- ); ++ }; + visit(root); + return calls; } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( -- "details", -- { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +-function 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 useShownCards(actions, sessionId, callIds) { + const key = JSON.stringify(callIds); @@ -9306,15 +8424,15 @@ diff --git a/client.js b/client.js + return () => actions.noteCards(sessionId, ids, -1); + }, [sessionId, key]); } --function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -- const tail = useTurnData?.("turn-tail"); -- const location2 = node.location; -- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -- const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { -- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -- }, [fileMentions, openFile, seq, tail, turn]); +-function useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; @@ -9325,7 +8443,65 @@ diff --git a/client.js b/client.js + const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; + if (done && call4.isError) return (0, import_react10.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); + return (0, import_react10.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -+ } + } +- 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]]; +- } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; +-} +-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}`); + case "stop_bot": { + const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + if (done && call4.isError) return (0, import_react10.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); @@ -9374,7 +8550,20 @@ diff --git a/client.js b/client.js + parsed = JSON.parse(contentText(call4.content)); + } catch { + parsed = null; -+ } + } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) +- ); +-} +- +-// src/client/icons.js +-var import_react3 = require("react"); +-function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); + const answer = parsed?.answers?.[0]; + const question = argValue("questions")?.[0]; + if (!answer) return null; @@ -9392,41 +8581,16 @@ diff --git a/client.js b/client.js + return null; + } } --function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -+function ToolCell({ node, sessionId, useRoster, useUi, actions }) { - const roster = useRoster((value) => value); -- const 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 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 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); @@ -9441,32 +8605,8 @@ diff --git a/client.js b/client.js + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react10.createElement)(import_react10.Fragment, null, drawn.map((call4, index) => (0, import_react10.createElement)(import_react10.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); } --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) -- ) -- ); -- } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); -- }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); +-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" })); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -9475,22 +8615,12 @@ diff --git a/client.js b/client.js + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; } --function 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 MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), +- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) - ); + +// src/client/remote-approval-wait.js @@ -9500,12 +8630,11 @@ diff --git a/client.js b/client.js + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; } --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 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 })) - ); + +// src/client/activity-row-host.js @@ -9517,18 +8646,11 @@ diff --git a/client.js b/client.js + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); } --function 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 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" }) - ); + +// src/client/activity-row.js @@ -9542,8 +8664,8 @@ diff --git a/client.js b/client.js + const hours = Math.floor(minutes / 60); + return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; } --function 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 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" })); +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -9556,11 +8678,14 @@ diff --git a/client.js b/client.js + }, [shown, at]); + return shown; } --var scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -- } -- return null; +-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 })) +- ); +function findHost(where) { + if (where !== TAIL_HOST) return findRunningHostIn(document); + const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]"); @@ -9575,25 +8700,78 @@ diff --git a/client.js b/client.js + for (const node of document.querySelectorAll(`[data-chat-flow-kind${kind ? `="${kind}"` : ""}][data-chat-turn]`)) top = Math.max(top, Number(node.dataset.chatTurn)); + return top; }; --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 })); +-var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; +-function CloudDownloadIcon({ done = false }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: CLOUD }), +- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) +- ); +-} +-var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); +-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); +-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); +-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); +-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); +-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); +-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); +-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); +-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); +-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); +-var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); +-var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); +-function PinMarkIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( +- "g", +- { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) +- ) +- ); +-} +-var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); +-var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); +-var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); +-var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); +-var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); +-var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); +-var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); +-var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +-var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); +-var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); +-var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +-var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); +-var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +- +-// src/client/questions.js +-var import_react4 = require("react"); +-var keyLetter = (index) => String.fromCharCode(65 + index); +-var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); +-var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; +-function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); +- pick2.current = onPick; +- (0, import_react4.useEffect)(() => { +- if (count === 0) return void 0; +- const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); +- } +function useReplyAfterSend(remote, sending) { + const sendTurn = (0, import_react11.useRef)(-1); + const wasSending = (0, import_react11.useRef)(false); @@ -9615,147 +8793,116 @@ diff --git a/client.js b/client.js + const check = () => { + frame = 0; + if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true); -+ }; + }; +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); - }); -- }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); ++ }); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + check(); - return () => { -- alive.current = false; ++ return () => { + cancelAnimationFrame(frame); + observer.disconnect(); - }; -- }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { -- const pending = restore.current; -- restore.current = null; -- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -- }, [view.items]); -- (0, import_react6.useEffect)(() => { -- const node = top.current; -- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -- const observer = new IntersectionObserver((entries2) => { -- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -- observer.observe(node); -- return () => observer.disconnect(); -- }, [view.cursor, view.loading, view.failed]); -- const selfId = roster.byId[sessionId]?.id; -- const rows = []; -- for (const item of view.items) { -- const last = rows.at(-1); -- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -- } -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( -- "div", -- { ref: top, className: "bt-earlier-top" }, -- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null -- ), -- rows.map((row, index) => { -- const key = rows.length - index; -- switch (row.kind) { -- case "divider": -- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); -- case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, -- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -- )); -- case "answer": -- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); -- case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -- case "event": { -- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); -- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -- if (selfId && other.id) actions.openExchange(selfId, other.id); -- }, whole: true }); -- } -- default: -- return null; -- } -- }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) -- ); ++ }; + }, [remote, sending]); + return replied; } --function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- const deferred = source.kind === "bot" && source.deferred === true; -- const relayed = isRelayContext(data); -- const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { -- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -- }, [relayed, sessionId, turn, source.senderSessionId]); -- const order = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -- }); -- const peers = useTurnClock((state) => { -- if (!relayed) return ""; -- const turns = state[sessionId] ?? {}; -- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const hiddenTurns = roster.exchangeTurns[sessionId]; -- const unreadOpening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], order, hiddenTurns); -- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -- }); -- const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -- function triggerBody() { -- if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; -- if (source.role === "secret" || deferred) return null; -- if (source.role === "post") { -- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; -- const room2 = roster.roomsById[sessionId]; -- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -- } -- if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -- } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -- const run = relayed ? commRunOf( -- order.split(",").filter(Boolean).map(Number), -- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -- turn, -- hiddenTurns -- ) : null; -- if (run === "folded") return null; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -- const bots = [sender]; -- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); +-function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); +- const options = optionsOf(question); +- const multi = question.multiSelect === true; +- const custom = own.trim(); +- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; +- const choose = (label) => { +- if (multi) { +- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +- return; +- } +- onAnswer({ selected: [label] }); +- }; +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); +- }; +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react11.useState)(null); + (0, import_react11.useEffect)(() => { + if (!active) { + setHost(null); + return void 0; -+ } + } +- }) : null; +- const rows = options.map((option, optionIndex) => { +- const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( +- "button", +- { +- key: option.label, +- type: "button", +- className: "bt-q-opt", +- "aria-pressed": multi ? selected : void 0, +- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), +- onClick: () => choose(option.label) +- }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( +- "span", +- { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null +- ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( +- "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); +- } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); + let frame = 0; + const find = () => { + frame = 0; @@ -9794,18 +8941,13 @@ diff --git a/client.js b/client.js + observer.disconnect(); + column = next; + if (column) observer.observe(column, { childList: true, subtree: true }); - } -- return (0, import_react6.createElement)(EventLine, { -- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -- bots, -- onOpen: (bot) => actions.openExchange(selfId, bot.id), -- whole: true ++ } + if (!column) return; + const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; + let last = -1; + items.forEach((item, index) => { + if (seen.has(item)) last = index; - }); ++ }); + const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); + for (const item of fresh) seen.add(item); + if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; @@ -9814,7 +8956,29 @@ diff --git a/client.js b/client.js + const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); + item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; + item.addEventListener("animationend", settle); -+ } + } +- }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) +- ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( +- "div", +- { className: "bt-q-custom" }, +- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), +- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null +- ) : null, +- foot +- ); + }; + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(scan); @@ -9825,7 +8989,24 @@ diff --git a/client.js b/client.js + observer.disconnect(); + }; + }, []); -+} + } +-function SettledQuestion({ question, answer }) { +- const options = optionsOf(question); +- const prompt = question?.question ?? ""; +- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; +- if (chosen.length === 0) { +- return (0, import_react4.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) +- ) +- ); +- } +- return (0, import_react4.createElement)( +function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { + const roster = useRoster((value) => value); + const running = useSessionStatus((map) => map.get(sessionId)?.running === true); @@ -9864,10 +9045,7 @@ diff --git a/client.js b/client.js + if (wait <= 0) { + make(); + return void 0; - } -- if (source.kind === "schedule") { -- const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); ++ } + const timer = setTimeout(make, wait); + return () => clearTimeout(timer); + }, [wantKey, shown]); @@ -9880,10 +9058,7 @@ diff --git a/client.js b/client.js + if (visible) { + setPhase("in"); + return void 0; - } -- if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -- } ++ } + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -9927,7 +9102,20 @@ diff --git a/client.js b/client.js + if (!host || phase === null || !shown) return null; + const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + return (0, import_react_dom2.createPortal)((0, import_react11.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) +- ); + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react11.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react11.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react11.createElement)( @@ -9942,8 +9130,23 @@ diff --git a/client.js b/client.js + ) + ), host); } --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 }); + +// src/client/dsh-offer.js +var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); @@ -9954,23 +9157,22 @@ diff --git a/client.js b/client.js + if (dsh.kind === "npm") return true; + return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } --function 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 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 }) +- ); + +// src/client/overlay-hooks.js +var import_react12 = require("react"); @@ -9990,8 +9192,12 @@ diff --git a/client.js b/client.js + }; + }, []); } --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 useDialogFocus(ref) { + (0, import_react12.useEffect)(() => { + const before = document.activeElement; @@ -10001,32 +9207,55 @@ diff --git a/client.js b/client.js + }; + }, []); } --var SEPARATOR_GAP_MS = 30 * 60 * 1e3; --function clockLabel(time, now = Date.now()) { -- const date = new Date(time); -- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -- if (days === 0) return t("Today {time}", { time: clock4 }); -- if (days === 1) return t("Yesterday {time}", { time: clock4 }); -- const sameYear = date.getFullYear() === new Date(now).getFullYear(); -- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; -+function usePaneLeft() { -+ const [paneLeft, setPaneLeft] = (0, import_react12.useState)(280); -+ (0, import_react12.useLayoutEffect)(() => { -+ const column = document.querySelector('[class*="_footArea"]')?.parentElement; -+ if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); -+ }, []); -+ return paneLeft; - } --function nextTurnStart(turns, turn) { -- let next; -- for (const key in turns) { -- const candidate = Number(key); -- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +-function 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; - } -- return next === void 0 ? void 0 : turns[next].first; -+ +- const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- key: question.id, +- question, +- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +- }))); ++function usePaneLeft() { ++ const [paneLeft, setPaneLeft] = (0, import_react12.useState)(280); ++ (0, import_react12.useLayoutEffect)(() => { ++ const column = document.querySelector('[class*="_footArea"]')?.parentElement; ++ if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); ++ }, []); ++ return paneLeft; + } + +-// src/client/secret-card.js +-var import_react5 = require("react"); +-var MIN_VALUE = 4; +-var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), +- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) +-); +-var LockIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), +- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) +-); +-var WarnIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), +- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) +-); +-var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); +-var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); +-var scopeLabel = (scope, roster) => { +- if (scope === "all") return t("All Bots"); +- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); +- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); +// src/client/member-status.js +var import_react13 = require("react"); +var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -10034,7 +9263,31 @@ diff --git a/client.js b/client.js +var stepIn = (work, botId, roomId, tool) => { + const own = work.bots?.[botId]; + return own?.roomId === roomId && own.tool === tool ? own.step : void 0; -+}; + }; +-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; +-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +- } +var workIn = (room2, work, id) => { + const local = (work.rooms?.[room2.id] ?? []).find((entry) => entry.botId === id); + if (local) return { ...local, roomId: room2.id, step: stepIn(work, id, room2.id, local.tool) }; @@ -10048,114 +9301,37 @@ diff --git a/client.js b/client.js + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; + }); - } --function openingTurn(turns, firstTurns, hiddenTurns) { -- if (!turns || !firstTurns) return null; -- const start = nextTurnStart(turns, -Infinity); -- if (start === void 0) return null; -- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -- const listed = firstTurns.split(",").map(Number); -- const earlier = listed.filter((entry) => entry < turn); -- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; ++} +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); - } --var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; --function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -- const turn = location2.turn; -- const time = location2.end.time; -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "end", time); -- }, [sessionId, turn, time]); -- const label = useTurnClock((state) => { -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- if (typeof visit?.seen !== "number" || time > visit.seen) return false; -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start > visit.seen && start <= visit.entered; -- }); -- if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null -- ); ++} +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); + return { rows, rest: busy.filter((entry) => !rows.includes(entry)) }; - } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); ++} +function botGroupWork(botId, work, roster) { + return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => { + const entry = entries2.find((item) => item.botId === botId); + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; -+ }); + }); } --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 ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { +- key: id, +- type: "button", +- role: "radio", +- "aria-checked": value === id, +- className: "bt-secret-seg", +- onClick: () => onChange(id) +- }, text)) +var moreRooms = (count) => count === 1 ? t("1 more room") : t("{count} more rooms", { count }); +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); @@ -10165,24 +9341,16 @@ diff --git a/client.js b/client.js + const first = workers[0]; + const doing = first ? `${first.bot.name} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; + return { waiting, doing }; - } --function workPhrase(tool) { -- if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); ++} +function Face({ entry, size }) { + return (0, import_react13.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react13.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) -+ ); + ); } --function 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 SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( +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)}`; @@ -10194,42 +9362,7 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react13.createElement)(Shimmer, null, text2)), + elapsed ? (0, import_react13.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null + ); - } --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { -- if (done) return void 0; -- let stop = false; -- let timer; -- let seen = ""; -- const tick = async () => { -- const result = await actions.roomProgress(sessionId); -- if (stop) return; -- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; -- if (entry && key !== seen) { -- seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); -- } -- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); -- }; -- void tick(); -- return () => { -- stop = true; -- clearTimeout(timer); -- }; -- }, [done, sessionId, callId]); -- let replies = live.replies; -- if (done) { -- try { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; -- } -- } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; ++} +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -10250,8 +9383,7 @@ diff --git a/client.js b/client.js +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); - const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ const room2 = roster.roomsById[sessionId]; + const bot = room2 ? void 0 : roster.byId[sessionId]; + const ownChat = bot !== void 0 && work.bots?.[bot.id] !== void 0 && work.bots[bot.id].roomId === void 0; + const busy = room2 ? busyMembers(room2, work, roster) : []; @@ -10275,66 +9407,9 @@ diff --git a/client.js b/client.js + const more = groups.length - rows.length; + return (0, import_react13.createElement)( "div", -- { className: "bt-group-replies" }, -- replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -- if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); -- } -- if (reply.kind === "failed") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, -- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -- ); -- } -- if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -- ); -- } -- if (reply.kind === "missed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -- } -- if (reply.kind === "resumed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -- } -- if (reply.kind === "cut") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -- } -- if (reply.kind === "approval" || reply.kind === "working") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -- ) -- ); -- } -- if (reply.kind === "stopped") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); -- } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); -- }), -- pending.map((id) => { -- const bot = roster.byId[id]; -- if (!bot) return null; -- return (0, import_react6.createElement)( -- "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); -- }) +- { 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)( + { ref, className: "bt-mw-dock-stack" }, + rows.map((item) => (0, import_react13.createElement)(WorkRow, { + key: item.room.id, @@ -10347,26 +9422,26 @@ diff --git a/client.js b/client.js + onOpen: () => actions.openSession(item.room.id) + })), + more > 0 ? (0, import_react13.createElement)( -+ "div", + "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") +- ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + { className: "bt-mw-row bt-mw-line bt-mw-static" }, + (0, import_react13.createElement)("span", { className: "bt-mw-row-text" }, moreRooms(more)) + ) : null ); } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; -- const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); -- }; -- visit(root); -- return calls; +-function 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) => { +function useFlowColumn(ref, active, key) { + (0, import_react13.useLayoutEffect)(() => { + const node = ref.current; @@ -10383,15 +9458,7 @@ diff --git a/client.js b/client.js + for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); + return () => observer.disconnect(); + }, [active, key]); - } --function useShownCards(actions, sessionId, callIds) { -- const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { -- const ids = JSON.parse(key); -- if (ids.length === 0 || !actions.noteCards) return void 0; -- actions.noteCards(sessionId, ids, 1); -- return () => actions.noteCards(sessionId, ids, -1); -- }, [sessionId, key]); ++} +function useNow(active) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -10400,82 +9467,7 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [active]); + return now; - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -- } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -- } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; -- } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); -- } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -- } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; ++} +var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; +var alwaysChoices = (approval, bot) => bot?.remote ? [] : [ + ["always-bot", "Always allow for this Bot"], @@ -10484,29 +9476,75 @@ diff --git a/client.js b/client.js +function ApprovalCard({ approval, bot, actions }) { + const [busy, setBusy] = (0, import_react13.useState)(false); + const answer = (choice) => { -+ setBusy(true); + 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)); + void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); -+ }; + }; +- const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( + return (0, import_react13.createElement)( -+ "div", + "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)( + { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react13.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react13.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, + approval.reason ? (0, import_react13.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null, + approval.detail?.code ? (0, import_react13.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null, + approval.answerable ? (0, import_react13.createElement)( -+ "div", + "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)( + { className: "bt-mw-actions" }, + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")), + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")), + ...alwaysChoices(approval, bot).map(([choice, label]) => (0, import_react13.createElement)("button", { key: choice, type: "button", className: "bt-q-btn", "data-choice": choice, disabled: busy, onClick: () => answer(choice) }, t(label))) + ) : (0, import_react13.createElement)( -+ "div", + "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")) + { className: "bt-mw-actions" }, + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) -+ ) -+ ); -+} + ) + ); + } +-function SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react13.useState)(false); + if (entry.state === "ask") { @@ -10516,44 +9554,52 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), + entry.approvals.map((approval) => (0, import_react13.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); - } ++ } + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); + void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); + }; + return (0, import_react13.createElement)( -+ "div", + "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)( + { className: "bt-mw-member" }, + (0, import_react13.createElement)( -+ "div", + "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)) + { className: "bt-mw-state bt-mw-work" }, + (0, import_react13.createElement)("span", null, (0, import_react13.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react13.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null -+ ) -+ ); + ) + ); } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { -- const roster = useRoster((value) => value); -- const root = node.data.root; -- const turn = node.location?.turn?.turn ?? root.turn; -- const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); -- const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); -- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); -- if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +-function RequestView({ bot, request, roster, actions }) { +- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); +-} +-function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { +- const bot = roster.byId[sessionId]; +- if (!bot) return null; +- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); +- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); +- if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); +- } +- return null; +function useMemberWork(room2, useWork) { + const work = useWork?.((value) => value) ?? NO_WORK; + const entries2 = room2 ? memberWork(room2, work) : []; + const now = useNow(entries2.some((entry) => entry.state !== "idle")); + return { entries: entries2, now }; - } ++} +var MEMBER_CSS = ` +.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} +.bt-mw-row{pointer-events:auto;display:flex;align-items:center;gap:10px;min-width:0;max-width:calc(100% + 6px);height:32px;margin-inline-start:-6px;padding:0 12px 0 6px;border:0;border-radius:16px;background:none;color:var(--bt-ink-2);font:inherit;font-size:14px;cursor:pointer;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards;transition:background-color .16s ease-out} @@ -10588,17 +9634,7 @@ diff --git a/client.js b/client.js +@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} +@media (prefers-reduced-motion:reduce){.bt-mw-row{animation:none}} +`; - --// src/client/activity-row.js --var import_react7 = require("react"); --var import_react_dom2 = require("react-dom"); --var PHRASE_HOLD_MS = 800; --var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); --function elapsedLabel(ms) { -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${Math.max(1, minutes)}m`; -- const hours = Math.floor(minutes / 60); -- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++ +// src/shared/task-board.js +var COLUMNS = ["not-started", "in-progress", "done"]; +var PRIORITIES = ["urgent", "high", "normal", "low"]; @@ -10650,22 +9686,7 @@ diff --git a/client.js b/client.js + const value = task?.progress; + if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; + return Math.max(0, Math.min(100, Math.round(value))); - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { -- if (!active) { -- setHost(null); -- return void 0; -- } -- let frame = 0; -- const find = () => { -- frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -- setHost((prev) => prev === node ? prev : node); -- }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(find); ++} + +// src/client/inbox.js +var import_react14 = require("react"); @@ -10720,46 +9741,13 @@ diff --git a/client.js b/client.js + }).catch(() => { + }).finally(() => { + if (!stopped) timer = setTimeout(load, POLL_MS3); - }); -- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -- find(); ++ }); + load(); - return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- }; -- }, [active]); -- return host; --} --function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); -- check.current = replacing; -- (0, import_react7.useEffect)(() => { -- const seen = /* @__PURE__ */ new WeakSet(); -- const started = Date.now(); -- let column = null; -- let frame = 0; -- const settle = (event) => { -- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -- delete event.currentTarget.dataset.btEnter; -- event.currentTarget.removeEventListener("animationend", settle); ++ return () => { + stopped = true; + clearTimeout(timer); + taskListeners.delete(setTasks); - }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; ++ }; + }, []); + return tasks; +} @@ -10777,15 +9765,7 @@ diff --git a/client.js b/client.js + workflowListeners.add(setWorkflows); + if (workflowPoll === null && typeof actions?.workflowState === "function") { + const load = () => Promise.resolve(actions.workflowState()).then(publishWorkflows, () => { - }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); ++ }); + load(); + workflowPoll = setInterval(load, POLL_MS3); + } @@ -10794,10 +9774,8 @@ diff --git a/client.js b/client.js + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; - } - }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(scan); ++ } ++ }; + }, []); + return workflows; +} @@ -10870,11 +9848,7 @@ diff --git a/client.js b/client.js + chat: approval.roomId ?? bot.id, + approval, + ...people(roster, bot.id) - }); -- scan(); -- return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); ++ }); + } + for (const [key, question] of Object.entries(roster.questions ?? {})) { + if (question == null || typeof question !== "object") continue; @@ -10977,8 +9951,7 @@ diff --git a/client.js b/client.js + chat: askedIn2 ?? chatOfTask(task), + task, + ...people(roster, task.workBotId ?? bot.id) - }; -- }, []); ++ }; + if (task.status === "failed") { + if (fresh(task.updatedAt)) needs.push({ ...card, status: "failed", reason: lastWhy(task) }); + } else if (task.status === "result-ready") needs.push({ ...card, status: "result", reason: task.result?.summary ?? "" }); @@ -11070,35 +10043,7 @@ diff --git a/client.js b/client.js + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 0; - } --function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -- const roster = useRoster((value) => value); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -- const streaming = entries2.some((entry) => entry.text); -- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -- const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); -- const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); -- const inputAt = Math.max(inputRef.current, pendingInput); -- inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); -- const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); -- if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { -- if (wantKey === null || shown?.key === wantKey) return void 0; -- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); -- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -- if (wait <= 0) { -- make(); -- return void 0; ++} +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -11169,19 +10114,7 @@ diff --git a/client.js b/client.js + if (selected.length === 0 && custom === "") return; + onAnswer(card, { selected, custom }); + return; - } -- const timer = setTimeout(make, wait); -- return () => clearTimeout(timer); -- }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); -- leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { -- if (visible) { -- setPhase("in"); -- return void 0; ++ } + if (custom === "") return; + onAnswer(card, custom); + }; @@ -11190,39 +10123,7 @@ diff --git a/client.js b/client.js + if (!multi) { + onAnswer(card, { selected: [label], custom: "" }); + return; - } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); -- return () => clearTimeout(timer); -- }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { -- if (phase !== null) return; -- endedAt.current = Date.now(); -- setShown(null); -- const before = shift.current; -- shift.current = null; -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (before === null || !scroller || reducedMotion()) return; -- const delta = before - scroller.scrollTop; -- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); -- }, [phase]); -- (0, import_react7.useEffect)(() => { -- if (!running) actions.clearLive(sessionId); -- }, [running, sessionId]); -- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { -- if (phase !== "in") return void 0; -- const timer = setInterval(() => setNow(Date.now()), 1e4); -- return () => clearInterval(timer); -- }, [phase]); -- const host = useRunningHost(running); -- if (!host || phase === null || !shown) return null; -- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; -- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( ++ } + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -11256,9 +10157,40 @@ diff --git a/client.js b/client.js + }), + (0, import_react14.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); -+} + } +-function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { -+ const roster = useRoster((value) => value); + const roster = useRoster((value) => value); +- const shown = useCards?.((state) => state[sessionId]) ?? NONE; +- const loading = useSession?.(historyLoading) === true; +- const bot = roster.byId[sessionId]; +- if (!bot || currentPart(bot) !== sessionId) return null; +- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); +- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); +- if (waiting.length === 0) return null; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); +-} +-function ScopeEditor({ secret, roster, onSave }) { +- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); +- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); +- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( +- "label", +- { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text +- )) +- ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { +- key: bot.id, + const work = useWork ? useWork((value) => value) : {}; + const open = useUi ? useUi((value) => Boolean(value.inbox)) : false; + const tasks = useTasks(actions); @@ -11268,7 +10200,16 @@ diff --git a/client.js b/client.js + return (0, import_react14.createElement)( + "button", + { -+ type: "button", + type: "button", +- className: "bt-secret-bot", +- "aria-pressed": picked.includes(bot.id), +- onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) +- ) + className: "bt-row bt-inbox-row", + "aria-label": label, + title: wide ? void 0 : label, @@ -11279,14 +10220,52 @@ diff --git a/client.js b/client.js + (0, import_react14.createElement)("span", { className: "bt-inbox-mark" }, (0, import_react14.createElement)(InboxIcon)), + wide ? (0, import_react14.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react14.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null -+ ); -+} -+function Status({ status }) { -+ const { label, tone, Icon } = STATUSES[status]; -+ return (0, import_react14.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react14.createElement)(Icon), (0, import_react14.createElement)("span", null, t(label))); -+} -+function whenText(card, now) { -+ if (card.status === "running" || card.status === "task") { + ); + } +-function ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!long) return; +- void Promise.resolve(onSave(value)).then((ok) => { +- if (ok) { +- setValue(""); +- setReplaced(true); +- } +- }); +- }; +- const edit = (next) => { +- setValue(next); +- setReplaced(false); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-inline" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) +- ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null +- ); ++function Status({ status }) { ++ const { label, tone, Icon } = STATUSES[status]; ++ return (0, import_react14.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react14.createElement)(Icon), (0, import_react14.createElement)("span", null, t(label))); + } +-function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( ++function whenText(card, now) { ++ if (card.status === "running" || card.status === "task") { + return typeof card.since === "number" && now - card.since >= 6e4 ? t("for {time}", { time: elapsedLabel(now - card.since) }) : t("just started"); + } + return typeof card.at === "number" ? relativeShort(card.at) : ""; @@ -11304,25 +10283,30 @@ diff --git a/client.js b/client.js + const unconfirmed = card.kind === "task" && card.task?.writeBack?.state === "unconfirmed" ? card.task.writeBack : void 0; + return (0, import_react14.createElement)( "div", -- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, -- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react7.createElement)( +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( + { className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status, "data-run": card.kind === "workflow" ? card.run?.id : void 0 }, + (0, import_react14.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react14.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react14.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null), + (0, import_react14.createElement)( - "span", -- { className: "bt-typing-text" }, -- (0, import_react7.createElement)( ++ "span", + { className: "bt-inbox-copy" }, + (0, import_react14.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 +- { 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) + { className: "bt-inbox-top" }, + (0, import_react14.createElement)(Status, { status: card.status }), + (0, import_react14.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta) -+ ), + ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) + (0, import_react14.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title), + card.calls > 0 ? (0, import_react14.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null, + card.reason ? (0, import_react14.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, failureSentence(card.reason, t)) : null, @@ -11348,7 +10332,22 @@ diff --git a/client.js b/client.js + unconfirmed ? (0, import_react14.createElement)("span", { className: "bt-inbox-writeback", title: unconfirmed.target }, t("Could not confirm the comment on {target} went out", { target: unconfirmed.target })) : null, + ...locked ? [] : [(0, import_react14.createElement)(AnswerFields, { card, busy, onAnswer }), (0, import_react14.createElement)(ResultActions, { card, busy, onAnswer })], + error ? (0, import_react14.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null -+ ), + ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null + (0, import_react14.createElement)( + "span", + { className: "bt-inbox-actions" }, @@ -11357,8 +10356,21 @@ diff --git a/client.js b/client.js + ...(answerable ? alwaysChoices(card.approval, card.bot) : []).map(([choice, label]) => (0, import_react14.createElement)("button", { key: choice, type: "button", className: "bt-soft bt-soft-sm", "data-choice": choice, disabled: busy, onClick: () => onAnswer(card, choice) }, t(label))), + (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) -+ ); -+} + ); + } +-function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); +- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!ready) return; +- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { +- if (ok) { +- setValue(""); +- onDone(); +- } +- }); +function useNow2(ms) { + const [now, setNow] = (0, import_react14.useState)(Date.now); + (0, import_react14.useEffect)(() => { @@ -11409,14 +10421,24 @@ diff --git a/client.js b/client.js + const name = card.bot?.name ?? ""; + setFeedback(card.kind === "approval" ? choice === "allow" ? t("Allowed once for {name}", { name }) : choice === "always-bot" ? t("Always allowed for {name}", { name }) : choice === "always-room" ? t("Always allowed in this group") : t("Rejected for {name}", { name }) : t("Answered")); + }, (failure) => setErrors((value) => ({ ...value, [card.id]: hostText(failure?.message ?? String(failure)) }))).finally(() => setBusy("")); -+ }; + }; +- return (0, import_react5.createElement)( + const open = (card) => openCard(actions, card); + return (0, import_react14.createElement)( -+ "div", + "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)( + { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 }, + (0, import_react14.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react14.createElement)( -+ "div", + "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")) + { className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" }, + (0, import_react14.createElement)( + "div", @@ -11465,11 +10487,96 @@ diff --git a/client.js b/client.js + }, t(allDone ? "Show fewer" : "Show all")) : null + ); + }) - ) ++ ) ) -- ), host); -+ ); + ); } +-function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; +-} +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +var INBOX_CSS = ` +.bt-inbox-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-inbox-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -11522,16 +10629,13 @@ diff --git a/client.js b/client.js +.bt-inbox-answer button[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink)} +.bt-inbox-actions{flex:none;display:flex;align-items:center;gap:6px;align-self:center} +.bt-inbox-actions .bt-send{height:28px;font-size:12px} -+`; + `; --// src/client/dsh-offer.js --var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); --var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; --var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); --function showsDshRow(dsh, app) { -- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -- if (dsh.kind === "npm") return true; -- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); +-// src/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/shared/board-card.js +function taskCardKey(id) { + const text2 = String(id ?? "").trim(); @@ -11610,60 +10714,104 @@ diff --git a/client.js b/client.js + actions + ); } +-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/sidebar.js --var import_react8 = require("react"); --var import_react_dom3 = require("react-dom"); --var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); --function useMarkedUnread(id, active, marked, actions) { -- (0, import_react8.useEffect)(() => { -- if (active && marked) actions.clearUnread(id); -- }, [active, marked]); -+// src/client/kanban.js -+var import_react16 = require("react"); -+var POLL_MS4 = 15e3; -+function BoardIcon() { -+ return (0, import_react16.createElement)( -+ "svg", -+ { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, -+ (0, import_react16.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), -+ (0, import_react16.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), -+ (0, import_react16.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) -+ ); - } --var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); --var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; --var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; --var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { -- const [id, tool] = key.split("|"); -- return { id, tool: tool || void 0 }; --}); --var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); --function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { -- const id = bot.id; -- const part = currentPart(bot); -- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); -- const running = useSessionStatus((map) => map.get(part)?.running === true); -- const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); -- const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; -- useMarkedUnread(id, active, marked, actions); -- const main = isMainOf(roster, id); -- const tile = roster.mainBotId === id; -- const approval = useApproval(useWork, (entry) => entry.botId === id); -- const alert = roster.questions?.[part] !== void 0 || approval; -- const doing = useActivity((map) => map[id]); -- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); -- const away = !running && groups.length > 0; -- const state = botState(running || away, doing, alert); -- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; -- if (bot.hidden && !active && !main) return null; -- const onContextMenu = (event) => { +-// 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(); -- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); +- 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/kanban.js ++var import_react16 = require("react"); ++var POLL_MS4 = 15e3; ++function BoardIcon() { ++ return (0, import_react16.createElement)( ++ "svg", ++ { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, ++ (0, import_react16.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), ++ (0, import_react16.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), ++ (0, import_react16.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) + ); + } +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +function shortId(id) { + const hex = String(id ?? "").replace(/^task-/, ""); + return `#${hex.slice(-6)}`; -+} + } +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; +var ARCHIVE_SHOWN = 20; +function boardTasksFor(tasks, archive, limit = ARCHIVE_SHOWN) { + const newest = [...archive ?? []].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0) || (b.createdAt ?? 0) - (a.createdAt ?? 0)); @@ -11725,27 +10873,23 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); -+ } + } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); +- }); }; -- const pinned = bot.pinned === true && !tile; -- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); -- if (tile && wide) { -- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-tile", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-bot-id": id, -- title: preview || bot.name, -- onClick: () => actions.openSession(id), -- onContextMenu -- }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), -- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), -- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null -- )); +- 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); + walk("", 0); + const groups = (roster?.rooms ?? []).filter((room2) => room2 && room2.hidden !== true && room2.origin !== "face-to-face" && adminOf(roster, room2)).map((room2) => ({ kind: "group", id: room2.id, name: room2.displayName || room2.name || t("Group"), room: room2 })); + return { bots: rows, groups }; @@ -11786,8 +10930,7 @@ diff --git a/client.js b/client.js + return t("Failed"); + default: + return t("Not started"); - } -- return (0, import_react8.createElement)( ++ } +} +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); @@ -11851,22 +10994,26 @@ diff --git a/client.js b/client.js + if (scope?.kind === "group") setGroupTasks((list) => done2 ? without(list, task.id) : upsert(list, task)); + else setOwn((list) => done2 ? without(list, task.id) : upsert(list, task)); + setArchive((list) => done2 ? [task, ...without(list, task.id)] : without(list, task.id)); -+ }; + }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { +- close(); +- run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( +- "div", +- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( + const done = scope?.kind === "group" ? archive : archive.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + return { own, groupTasks, tasks, archive: done, replace }; +} +function RailButton({ row, selected, meta, onSelect }) { + return (0, import_react16.createElement)( - "button", - { - type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-bot-id": id, -- title: wide ? void 0 : bot.name, -- onClick: () => actions.openSession(id), -- onContextMenu ++ "button", ++ { ++ type: "button", + className: "bt-board-rail-item", + "data-kind": row.kind, + "data-id": row.id, @@ -11874,47 +11021,11 @@ diff --git a/client.js b/client.js + "aria-current": selected ? "true" : void 0, + style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : void 0, + onClick: () => onSelect(row.kind === "group" ? { kind: "group", roomId: row.id } : { kind: "bot", botId: row.id }) - }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( -- "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ }, + (0, import_react16.createElement)("span", { className: "bt-board-rail-name" }, row.name), + meta ? (0, import_react16.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null - ); - } --function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { -- const id = room2.id; -- const fallback = useSessions((list) => previewOf(list, id)); -- const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; -- const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); -- const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; -- useMarkedUnread(id, active, marked, actions); -- const approval = useApproval(useWork, (entry) => entry.roomId === id); -- const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); -- const first = roster.byId[members[0]?.id]; -- const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; -- if (room2.hidden && !active) return null; -- const pinned = room2.pinned === true; -- const label = [room2.name, pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", approval ? t("Waiting for your approval") : ""].filter(Boolean).join(", "); -- return (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-room-id": id, -- onClick: () => actions.openSession(id), -- onContextMenu: (event) => { -- event.preventDefault(); -- actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); -- } ++ ); ++} +function BoardCard2({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { + const column = columnOf(task); + const priority = priorityOf(task); @@ -11931,10 +11042,7 @@ diff --git a/client.js b/client.js + "data-column": column, + "data-priority": priority, + "data-kind": overall ? "group" : void 0 - }, -- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( ++ }, + idText: shortId(task.id), + emptyTitle: t("Untitled task"), + topExtra: [ @@ -11948,13 +11056,7 @@ diff --git a/client.js b/client.js + question ? (0, import_react16.createElement)("span", { key: "wait", className: "bt-board-wait" }, question) : null + ], + footer: (0, import_react16.createElement)( - "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -- ); ++ "span", + { className: "bt-board-foot" }, + who ? (0, import_react16.createElement)("span", { className: "bt-board-who" }, who) : null, + Number.isFinite(finished) && finished > 0 ? (0, import_react16.createElement)("span", { key: "finished", className: "bt-board-when" }, relativeShort(finished)) : null, @@ -11978,48 +11080,7 @@ diff --git a/client.js b/client.js + onDragStart: () => onDrag?.(task.id), + onDragEnd: () => onDrag?.(null) + }); - } --function AgentSessionRow({ session, agent, actions, renaming, index }) { -- const input = (0, import_react8.useRef)(null); -- (0, import_react8.useLayoutEffect)(() => { -- if (!renaming) return; -- input.current?.focus(); -- input.current?.select(); -- }, [renaming]); -- if (renaming) { -- const commit = (keep) => { -- const value = input.current?.value ?? ""; -- if (!keep || value.trim() === "" || value.trim() === session.title) { -- actions.endAgentRename(); -- return; -- } -- void (async () => { -- try { -- await actions.renameAgentSession(agent.id, session.id, value); -- actions.endAgentRename(); -- } catch (error) { -- actions.reportError(error, "agent-rename"); -- } -- })(); -- }; -- return (0, import_react8.createElement)( -- "div", -- { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, -- (0, import_react8.createElement)("input", { -- ref: input, -- defaultValue: session.title, -- "aria-label": t("Rename"), -- onKeyDown: (event) => { -- if (event.nativeEvent.isComposing) return; -- if (event.key === "Enter") commit(true); -- else if (event.key === "Escape") commit(false); -- }, -- onBlur: () => commit(true) -- }) -- ); -- } -- return (0, import_react8.createElement)( -- "button", ++} +function cardProps(task, roster, onMove, onDrag, partOf2, locked) { + return { task, roster, movable: !locked && canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; +} @@ -12029,21 +11090,13 @@ diff --git a/client.js b/client.js + const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; + return (0, import_react16.createElement)( + "section", - { -- type: "button", -- className: "bt-agent-sub-row", -- "aria-current": session.active ? "page" : void 0, -- style: { "--i": index }, -- onClick: () => actions.openAgentSession(session.id), -- onContextMenu: (event) => { ++ { + className: "bt-board-column", + "data-column": column, + "aria-label": columnTitle(column), + onDragOver: (event) => { + if (locked || !dragging) return; - event.preventDefault(); -- event.stopPropagation(); -- actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); ++ event.preventDefault(); + }, + onDrop: (event) => { + if (locked) return; @@ -12051,10 +11104,8 @@ diff --git a/client.js b/client.js + const id = event.dataTransfer?.getData("text/plain"); + if (id) onMove?.(id, column); + onDrag?.(null); - } - }, -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ++ } ++ }, + (0, import_react16.createElement)( + "h3", + { className: "bt-board-column-head" }, @@ -12066,7 +11117,14 @@ diff --git a/client.js b/client.js + dragging ? (0, import_react16.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, + tasks.length === 0 && !dragging ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, + tree ? tree.nests.map((nest) => (0, import_react16.createElement)( -+ "div", + "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)( + { key: nest.task.id, className: "bt-board-nest" }, + (0, import_react16.createElement)(BoardCard2, cardProps(nest.task, roster, onMove, onDrag, void 0, locked)), + nest.children.map((child) => (0, import_react16.createElement)( @@ -12077,77 +11135,30 @@ diff --git a/client.js b/client.js + )) : null, + linked.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "", locked) })), + lanes ? lanesOf(plain, roster).map((lane) => (0, import_react16.createElement)( -+ "div", + "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 + { key: lane.id, className: "bt-board-lane" }, + (0, import_react16.createElement)("span", { className: "bt-board-lane-name" }, lane.name), + lane.tasks.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) + )) : plain.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) ); } --function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -- const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { -- const row = list.byId[id]; -- return { -- id, -- // A blank Session's title is the workspace folder name; show the label instead. -- title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", -- at: row?.updatedAt ?? 0, -- running: row?.running === true, -- active: (row?.retainedBy?.mainView ?? 0) > 0 -- }; -- }).sort((a, b) => b.at - a.at))); -- const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ -- id, -- running: map.get(id)?.running === true, -- unread: map.get(id)?.completionUnread === true -- })))); -- const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); -- const sessions = JSON.parse(info).map((session) => ({ -- ...session, -- running: statuses[session.id]?.running ?? session.running, -- unread: statuses[session.id]?.unread ?? false, -- title: session.title === "" ? t("New session") : session.title -- })); -- const renaming = useUi((value) => value.renameAgent); -- const active = sessions.some((session) => session.active); -- const running = sessions.some((session) => session.running); -- const unread = sessions.some((session) => session.unread) || marked && !active; -- useMarkedUnread(agent.id, active, marked, actions); -- const preview = sessions[0]?.title ?? t("New session"); -- const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); -- const open = active && wide; -- return (0, import_react8.createElement)( +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +- "details", +- { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate, archiveFull, locked }) { + const columns = boardColumns(tasks); + const lanes = scope?.kind === "group"; + const selectedBot = scope?.kind === "bot" ? scope.botId : ""; + const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; + return (0, import_react16.createElement)( - "div", -- { className: "bt-agent" }, -- (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-agent-id": agent.id, -- title: wide ? void 0 : "DSH Agent", -- onClick: () => actions.openAgent(agent.id), -- onContextMenu: (event) => { -- event.preventDefault(); -- actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); -- } -- }, -- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), -- wide ? (0, import_react8.createElement)( -- "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), -- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ "div", + { className: "bt-board-body" }, + (0, import_react16.createElement)( + "aside", @@ -12167,12 +11178,9 @@ diff --git a/client.js b/client.js + }), + rail.groups.length > 0 ? (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, + rail.groups.map((row) => (0, import_react16.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) - ), -- (0, import_react8.createElement)( ++ ), + (0, import_react16.createElement)( - "div", -- { className: "bt-agent-sub", "data-open": open || void 0 }, -- (0, import_react8.createElement)( ++ "div", + { className: "bt-board-main" }, + error ? (0, import_react16.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, + lanes ? (0, import_react16.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, @@ -12189,23 +11197,7 @@ diff --git a/client.js b/client.js + // so the lock's own sentence is drawn here too, from the host, not in a second wording. + locked ? (0, import_react16.createElement)("p", { className: "bt-board-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + (0, import_react16.createElement)( - "div", -- { className: "bt-agent-sub-in" }, -- (0, import_react8.createElement)( -- "button", -- { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { -- void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); -- } }, -- (0, import_react8.createElement)(PlusIcon), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) -- ), -- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { -- key: session.id, -- session, -- agent, -- actions, -- index: index + 1, -- renaming: renaming === session.id ++ "div", + { className: "bt-board-columns" }, + ["not-started", "in-progress", "done"].map((column) => (0, import_react16.createElement)(BoardColumn, { + key: column, @@ -12218,30 +11210,54 @@ diff --git a/client.js b/client.js + onDrag, + archiveFull, + locked - })) - ) - ) ++ })) ++ ) ++ ) ); } --var GLIDE = springEasing(1e3, 63); --function useListGlide(listRef, layout, key) { -- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); -- (0, import_react8.useLayoutEffect)(() => { -- const list = listRef.current; -- if (!list) return; -- const next = /* @__PURE__ */ new Map(); -- const still = reducedMotion() || tops.current.layout !== layout; -- for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { -- const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; -- const top = row.offsetTop; -- next.set(id, top); -- const before = tops.current.map.get(id); -- if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { -- row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); -- } -- } -- tops.current = { layout, map: next }; -- }, [layout, key]); +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; +- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; +- }, [fileMentions, openFile, seq, tail, turn]); +-} +-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- const running = data.status === "running"; +- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); +- if (hidden) return null; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; +- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true +- }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +function KanbanRow({ wide, useUi, actions }) { + const open = useUi ? useUi((value) => Boolean(value.board)) : false; + const label = t("Tasks"); @@ -12258,43 +11274,16 @@ diff --git a/client.js b/client.js + }, + (0, import_react16.createElement)("span", { className: "bt-board-mark" }, (0, import_react16.createElement)(BoardIcon)), + wide ? (0, import_react16.createElement)("span", { className: "bt-board-label" }, label) : null -+ ); -+} + ); + } +-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { +- if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( +function scopeName(roster, scope) { + if (scope?.kind === "group") return roster.roomsById?.[scope.roomId]?.name || t("Group"); + if (scope?.kind === "bot") return roster.byId?.[scope.botId]?.name || ""; + return ""; - } --function TeamSidebar(props) { -- const { wide, useRoster, useUi, useSessions, actions } = props; -- const listRef = (0, import_react8.useRef)(null); -- const roster = useRoster((value) => value); -- const markedUnread = useUi((value) => value.unread); -- props.useRegistry((value) => value); -- const updated = useSessions((list) => { -- const stamps = {}; -- for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; -- return JSON.stringify(stamps); -- }); -- const order = (0, import_react8.useMemo)(() => { -- const stamps = JSON.parse(updated); -- const entries2 = [ -- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), -- ...roster.rooms.map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), -- // The Agent sorts by its most recent Session, falling back to when it was added. -- ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) -- ]; -- return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || b.at - a.at); -- }, [roster, updated]); -- const main = roster.byId[roster.mainBotId]; -- useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); -- useLandOnMainBot(roster, useSessions, actions); -- const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -- const typeToSearch = (event) => { -- if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; -- if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; -- event.preventDefault(); -- actions.openPalette(event.key); ++} +function KanbanDialog({ roster, actions, leaving = false }) { + const locked = typeof roster?.readOnly === "string" && roster.readOnly.trim() !== ""; + const rail = boardRail(roster); @@ -12334,8 +11323,7 @@ diff --git a/client.js b/client.js + } catch (failure) { + setError(failure?.message ?? String(failure)); + } - }; -- return (0, import_react8.createElement)( ++ }; + const create = async (title) => { + if (locked) return; + const text2 = String(title ?? "").trim(); @@ -12350,30 +11338,20 @@ diff --git a/client.js b/client.js + }; + const name = scopeName(roster, scope); + return (0, import_react16.createElement)( - "div", -- { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, -- wide ? (0, import_react8.createElement)( -- "div", -- { className: "bt-side-top" }, -- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -- ) : (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -- ), -- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, -- (0, import_react8.createElement)( ++ "div", + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react16.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react16.createElement)( "div", -- { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, -- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), -- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) +- { className: "bt-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) +- ) +- ); + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react16.createElement)( + "div", @@ -12402,18 +11380,9 @@ diff --git a/client.js b/client.js + onDrag: setDragging, + onCreate: create + }) - ) - ); - } --function UpdateButton({ useUpdate, actions }) { -- const update = useUpdate((value) => value); -- const [open, setOpen] = (0, import_react8.useState)(false); -- const [box, setBox] = (0, import_react8.useState)(null); -- const [error, setError] = (0, import_react8.useState)(""); -- const [dshError, setDshError] = (0, import_react8.useState)(""); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (!open) return void 0; ++ ) ++ ); ++} +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -12521,17 +11490,35 @@ diff --git a/client.js b/client.js + for (const role of choices) { + if (role === current) continue; + items.push({ role, label: roleText(role), current: false, pick: true }); -+ } + } +- const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( +- "button", +- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- bot.name +- )); +- }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + return items; +} +function 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 GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react17.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react17.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react17.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react17.createElement)( -+ "button", + "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)( + { + key: String(project.id), + type: "button", @@ -12549,62 +11536,134 @@ diff --git a/client.js b/client.js + roleText(project.role) === "" ? null : (0, import_react17.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role)) + )); + return (0, import_react17.createElement)( -+ "div", + "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 }) + { className: "bt-projects-list", role: "group", "aria-label": t("Projects") }, + rows, + note === "" || note === void 0 ? null : (0, import_react17.createElement)("div", { className: "bt-projects-note", role: "status" }, note) -+ ); -+} + ); + } +-function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null +function Chevron() { + return (0, import_react17.createElement)( + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react17.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }) -+ ); -+} + ); + } +-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- const target = other && other.id !== self?.id ? other : self; +- if (!target) return null; +- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) +function ProjectsMark() { + return (0, import_react17.createElement)( + "svg", + { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react17.createElement)("path", { d: "M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z" }), + (0, import_react17.createElement)("path", { d: "M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2" }) -+ ); -+} + ); + } +-function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +-} +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; +- } +- return null; +-}; +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +- }); +- }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); +function ProjectRoleMenu({ project, x, y, onPick, onClose }) { + const ref = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - const onDown = (event) => { -- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -- setOpen(false); ++ const onDown = (event) => { + if (!ref.current?.contains(event.target)) onClose(); - }; - const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setOpen(false); -- button.current?.focus(); -- } ++ }; ++ const onKey = (event) => { + if (event.key === "Escape") onClose(); - }; -- window.addEventListener("mousedown", onDown); ++ }; + window.addEventListener("mousedown", onDown, true); - window.addEventListener("keydown", onKey, true); ++ window.addEventListener("keydown", onKey, true); return () => { -- window.removeEventListener("mousedown", onDown); +- alive.current = false; + window.removeEventListener("mousedown", onDown, true); - window.removeEventListener("keydown", onKey, true); ++ window.removeEventListener("keydown", onKey, true); }; -- }, [open]); -- const phase = update?.phase ?? "idle"; -- const dsh = update?.dsh; -- const dshPhase = dsh?.phase ?? "idle"; -- const dshRow = showsDshRow(dsh, update?.app); -- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; -- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; +- }, [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)( + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react17.createElement)( -+ "div", + "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 + { + ref, + className: "bt-project-menu", @@ -12657,83 +11716,14 @@ diff --git a/client.js b/client.js + if (!expanded) return; + void load(); + }, [expanded, load]); - const toggle = () => { -- const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); -- setOpen((value) => !value); -- }; -- const start = () => { -- setError(""); -- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); -- }; -- const startDsh = () => { -- setDshError(""); -- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); ++ const toggle = () => { + setNote(""); + if (!wide) { + void open(""); + return; + } + setExpanded((value) => !value); - }; -- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); -- const message = error || (phase === "failed" ? update.error : ""); -- const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); -- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); -- return (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)("button", { -- ref: button, -- type: "button", -- className: "bt-round bt-update", -- "data-phase": cloudPhase, -- "aria-label": label, -- title: label, -- "aria-haspopup": "dialog", -- "aria-expanded": open, -- onClick: toggle -- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -- "div", -- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, -- update?.available || phase !== "idle" ? (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), -- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -- (0, import_react8.createElement)( -- "div", -- { className: "bt-update-actions" }, -- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -- phase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: phase === "installing", -- onClick: start -- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) -- ) -- ) : null, -- dshRow ? (0, import_react8.createElement)( -- "div", -- { className: "bt-update-dsh" }, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), -- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -- (0, import_react8.createElement)( -- "div", -- { className: "bt-update-actions" }, -- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { -- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: dshPhase === "installing", -- onClick: startDsh -- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) -- ) -- ) : null -- ), document.body) : null ++ }; + const listed = expanded && wide; + return (0, import_react17.createElement)( + "div", @@ -12751,7 +11741,30 @@ diff --git a/client.js b/client.js + (0, import_react17.createElement)("span", { className: "bt-projects-mark" }, (0, import_react17.createElement)(ProjectsMark)), + wide ? (0, import_react17.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null, + wide ? (0, import_react17.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react17.createElement)(Chevron)) : null -+ ), + ), +- rows.map((row, index) => { +- const key = rows.length - index; +- switch (row.kind) { +- case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null +- )); +- case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); +- case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); +- case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); +- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; +- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { +- if (selfId && other.id) actions.openExchange(selfId, other.id); +- }, whole: true }); + listed ? (0, import_react17.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null, + menu === null ? null : (0, import_react17.createElement)(ProjectRoleMenu, { + project: menu.project, @@ -12767,32 +11780,85 @@ diff --git a/client.js b/client.js + if (answer?.changed === true) void load(); + } catch (failure) { + setNote(failure?.message ?? String(failure)); -+ } -+ } + } +- default: +- return null; + } +- }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) + }) ); } --function ConnectPlugins({ wide, actions }) { -- const tipId = (0, import_react8.useId)(); -- const [tip, setTip] = (0, import_react8.useState)(false); -- const timer = (0, import_react8.useRef)(0); -- const button = (0, import_react8.useRef)(null); -- const [box, setBox] = (0, import_react8.useState)(null); -- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); -- if (!wide) return null; -- const show = () => { -- clearTimeout(timer.current); -- timer.current = setTimeout(() => { -- const rect = button.current?.getBoundingClientRect(); -- if (!rect) return; -- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); -- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); -- setTip(true); -- }, 250); -- }; -- const hide = () => { -- clearTimeout(timer.current); -- setTip(false); +-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- const deferred = source.kind === "bot" && source.deferred === true; +- const relayed = isRelayContext(data); +- const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { +- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); +- }, [relayed, sessionId, turn, source.senderSessionId]); +- const order = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; +- }); +- const peers = useTurnClock((state) => { +- if (!relayed) return ""; +- const turns = state[sessionId] ?? {}; +- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const hiddenTurns = roster.exchangeTurns[sessionId]; +- const unreadOpening = useTurnClock((state) => { +- const first = openingTurn(state[sessionId], order, hiddenTurns); +- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; +- }); +- const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; +- function triggerBody() { +- if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; +- if (source.role === "secret" || deferred) return null; +- if (source.role === "post") { +- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; +- const room2 = roster.roomsById[sessionId]; +- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); +- } +- if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); +- } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); +- const run = relayed ? commRunOf( +- order.split(",").filter(Boolean).map(Number), +- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +- turn, +- hiddenTurns +- ) : null; +- if (run === "folded") return null; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); +- const bots = [sender]; +- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { +- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); +- } +- return (0, import_react6.createElement)(EventLine, { +- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), +- bots, +- onOpen: (bot) => actions.openExchange(selfId, bot.id), +- whole: true +- }); +- } +- if (source.kind === "schedule") { +- const prompts = reminderPrompts(contentText(data.content)); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); +- } +- 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")); + +// src/client/sidebar.js +var import_react18 = require("react"); @@ -12834,11 +11900,7 @@ diff --git a/client.js b/client.js + const onContextMenu = (event) => { + event.preventDefault(); + actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); - }; -- return (0, import_react8.createElement)( -- "span", -- { className: "bt-connect-wrap" }, -- (0, import_react8.createElement)( ++ }; + const pinned = bot.pinned === true && !tile; + const remote = Boolean(bot.remote); + const shown = remote ? friendBotTitle(bot) : bot.name; @@ -12846,17 +11908,9 @@ diff --git a/client.js b/client.js + const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); + if (tile && wide) { + return (0, import_react18.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react18.createElement)( - "button", - { -- ref: button, - type: "button", -- className: "bt-connect bt-connect-foot", -- "aria-describedby": tip ? tipId : void 0, -- onMouseEnter: show, -- onMouseLeave: hide, -- onFocus: show, -- onBlur: hide, -- onClick: () => actions.selectPanel("plugins") ++ "button", ++ { ++ type: "button", + className: "bt-tile", + "aria-label": label, + "aria-current": active ? "page" : void 0, @@ -12864,19 +11918,14 @@ diff --git a/client.js b/client.js + title: preview || bot.name, + onClick: () => actions.openSession(id), + onContextMenu - }, -- (0, import_react8.createElement)(PlugIcon), -- t("Connect plugins"), -- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") -- ), -- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ }, + (0, import_react18.createElement)(BotAvatar, { bot, size: 60, state, main: true }), + (0, import_react18.createElement)("span", { className: "bt-tile-name" }, shown), + // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. + unread && !waiting && !active ? (0, import_react18.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, + bot.role ? (0, import_react18.createElement)("span", { className: "bt-chip" }, bot.role) : null + )); -+ } + } + const unreadMark = wide && waiting && !active ? (0, import_react18.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react18.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; + const row = (0, import_react18.createElement)( + "button", @@ -12894,10 +11943,7 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)(BotAvatar, { bot, size: 36, state, main }), + pinned && !wide ? (0, import_react18.createElement)(PinMark) : null, + wide ? (0, import_react18.createElement)( - "span", -- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, -- t("Plugin compatibility is not fully verified.") -- ), document.body) : null ++ "span", + { className: "bt-row-body" }, + (0, import_react18.createElement)( + "span", @@ -12909,7 +11955,7 @@ diff --git a/client.js b/client.js + secondLine ? (0, import_react18.createElement)("span", { className: "bt-row-preview" }, secondLine) : null + ) : null, + unreadMark - ); ++ ); + const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); + const settings = wide ? (0, import_react18.createElement)("button", { + type: "button", @@ -12935,14 +11981,11 @@ diff --git a/client.js b/client.js + reset(); + if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); } --function YouAvatar() { -- return (0, import_react8.createElement)( +-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 RemoteMark({ remote }) { + return (0, import_react18.createElement)( - "span", -- { className: "bt-you-wrap" }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ "span", + { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, + (0, import_react18.createElement)( + "svg", @@ -12950,33 +11993,25 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react18.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ) - ); ++ ); } --var openShellSettings = null; --var openShellOnboarding = null; --var shellShortcuts = null; --function holdShellShortcuts(shortcuts) { -- shellShortcuts = shortcuts; -- return () => { -- if (shellShortcuts === shortcuts) shellShortcuts = null; -- }; --} --function openHarnessSettings() { -- requestAnimationFrame(() => { -- if (openShellSettings) { -- openShellSettings(); -- return; -- } -- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); -- if (desktop) openThroughMenu(desktop); -- else pressSettingsShortcut(); -- }); --} --function openHarnessOnboarding(id) { -- requestAnimationFrame(() => { -- if (openShellOnboarding) openShellOnboarding(id); -- else openHarnessSettings(); +-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- const own = state[sessionId]?.[turn]; +- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; - }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; +function isPastFaceRoom(room2) { + if (room2?.origin !== "face-to-face") return false; + const status = room2.face?.status; @@ -12984,18 +12019,8 @@ diff --git a/client.js b/client.js + const expires = room2.face?.expiresAt; + return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); } --function openThroughMenu(trigger) { -- const before = new Set(document.querySelectorAll('[role="menu"]')); -- trigger.click(); -- let frames = 0; -- const look = () => { -- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); -- const item = menu?.querySelector('button[role="menuitem"]'); -- if (item) item.click(); -- else if (++frames < 30) requestAnimationFrame(look); -- else pressSettingsShortcut(); -- }; -- requestAnimationFrame(look); +-function Nothing() { +- return null; +function FaceMark() { + return (0, import_react18.createElement)( + "span", @@ -13009,26 +12034,22 @@ diff --git a/client.js b/client.js + ) + ); } --function pressSettingsShortcut() { -- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; -- if (!binding) return; -- const held = new Set(binding.modifiers); -- window.dispatchEvent(new KeyboardEvent("keydown", { -- code: binding.code, -- key: binding.code === "Comma" ? "," : "", -- bubbles: true, -- cancelable: true, -- ctrlKey: held.has("control"), -- altKey: held.has("alt"), -- shiftKey: held.has("shift"), -- metaKey: held.has("meta") -- })); -+function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { -+ const id = room2.id; -+ const fallback = useSessions((list) => nameMentions(previewOf(list, id, [], [], botNames(roster)), roster)); -+ const preview = nameMentions(visibleText(room2.preview), roster) || fallback; -+ const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); -+ const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; +-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 RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { ++ const id = room2.id; ++ const fallback = useSessions((list) => nameMentions(previewOf(list, id, [], [], botNames(roster)), roster)); ++ const preview = nameMentions(visibleText(room2.preview), roster) || fallback; ++ const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); ++ const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; + useMarkedUnread(id, active, marked, actions); + const approval = useApproval(useWork, (entry) => entry.roomId === id); + const question = roster.questions?.[id] !== void 0; @@ -13072,37 +12093,11 @@ diff --git a/client.js b/client.js + wide && alert && !active ? (0, import_react18.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react18.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + ); } --function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { -- const [open, setOpen] = (0, import_react8.useState)(false); -- (0, import_react8.useLayoutEffect)(() => { -- if (!seat) return void 0; -- actions.seatAccount(true); -- return () => actions.seatAccount(false); -- }, [seat]); -- const [box, setBox] = (0, import_react8.useState)(null); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (settingsOpen) setOpen(false); -- }, [settingsOpen]); -- (0, import_react8.useEffect)(() => { -- if (!openSettings) return void 0; -- openShellSettings = openSettings; -- return () => { -- if (openShellSettings === openSettings) openShellSettings = null; -- }; -- }, [openSettings]); -- (0, import_react8.useEffect)(() => { -- if (!openOnboarding) return void 0; -- openShellOnboarding = openOnboarding; -- return () => { -- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; -- }; -- }, [openOnboarding]); -- (0, import_react8.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -- setOpen(false); +-function nextTurnStart(turns, turn) { +- let next; +- for (const key in turns) { +- const candidate = Number(key); +- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +function AgentSessionRow({ session, agent, actions, renaming, index }) { + const input = (0, import_react18.useRef)(null); + (0, import_react18.useLayoutEffect)(() => { @@ -13125,9 +12120,7 @@ diff --git a/client.js b/client.js + actions.reportError(error, "agent-rename"); + } + })(); - }; -- const onKey = (event) => { -- if (event.key === "Escape") { ++ }; + return (0, import_react18.createElement)( + "div", + { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, @@ -13143,7 +12136,8 @@ diff --git a/client.js b/client.js + onBlur: () => commit(true) + }) + ); -+ } + } +- return next === void 0 ? void 0 : turns[next].first; + return (0, import_react18.createElement)( + "button", + { @@ -13154,16 +12148,22 @@ diff --git a/client.js b/client.js + onClick: () => actions.openAgentSession(session.id), + onContextMenu: (event) => { + event.preventDefault(); - event.stopPropagation(); -- setOpen(false); -- button.current?.focus(); ++ event.stopPropagation(); + actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); - } ++ } + }, + (0, import_react18.createElement)("span", { className: "bt-agent-sub-title" }, session.title), + (0, import_react18.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") + ); -+} + } +-function 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 AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { + const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { + const row = list.byId[id]; @@ -13174,34 +12174,7 @@ diff --git a/client.js b/client.js + at: row?.updatedAt ?? 0, + running: row?.running === true, + active: (row?.retainedBy?.mainView ?? 0) > 0 - }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [open]); -- const toggle = () => { -- const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); -- setOpen((value) => !value); -- }; -- const groups = [ -- [ -- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], -- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], -- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], -- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] -- ], -- [ -- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] -- ] -- ]; -- return (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)( ++ }; + }).sort((a, b) => b.at - a.at))); + const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ + id, @@ -13227,9 +12200,7 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-agent" }, + (0, import_react18.createElement)( - "button", -- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ "button", + { + type: "button", + className: "bt-row", @@ -13251,26 +12222,9 @@ diff --git a/client.js b/client.js + preview ? (0, import_react18.createElement)("span", { className: "bt-row-preview" }, preview) : null + ) : null, + wide && unread && !active ? (0, import_react18.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null - ), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ), + (0, import_react18.createElement)( - "div", -- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, -- groups.map((group, index) => (0, import_react8.createElement)( -- import_react8.Fragment, -- { key: index }, -- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { -- key: label, -- type: "button", -- role: "menuitem", -- onClick: () => { -- setOpen(false); -- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); -- } -- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) -- )) -- ), document.body) : null ++ "div", + { className: "bt-agent-sub", "data-open": open || void 0 }, + (0, import_react18.createElement)( + "div", @@ -13293,26 +12247,28 @@ diff --git a/client.js b/client.js + })) + ) + ) - ); ++ ); } --function useLandOnMainBot(roster, useSessions, actions) { -- const mainView = useSessions((list) => { -- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); -- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; +-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 lastBot = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (!roster.ready || !roster.mainBotId) return void 0; -- const [id, blank3] = mainView.split("|"); -- const bot = id === "" ? void 0 : roster.byId[id]; -- if (bot !== void 0) { -- lastBot.current = { botId: bot.id, sessionId: id }; -- if (currentPart(bot) !== id) actions.landOn(bot.id); -- return void 0; -- } -- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { -- lastBot.current = null; -- return void 0; +- 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, +var GLIDE = springEasing(1e3, 63); +function useListGlide(listRef, layout, key) { + const tops = (0, import_react18.useRef)({ layout, map: /* @__PURE__ */ new Map() }); @@ -13329,224 +12285,100 @@ diff --git a/client.js b/client.js + if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { + row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); + } - } -- const last = lastBot.current; -- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); -- return () => clearTimeout(timer); -- }, [mainView, roster]); --} --function FooterAccount(props) { -- const seated = props.useUi((value) => value.accountSeated === true); -- if (seated) return null; -- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); --} --function BotReturn({ wide, useSurface, actions }) { -- const mode = useSurface((value) => value.mode); -- if (mode !== "agent") return null; -- return (0, import_react8.createElement)( -- "button", -- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, -- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), -- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null -- ); --} --var RETURN_CSS = ` --.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} --.bt-return:hover{background:rgba(127,127,127,.14)} --.bt-return:active{transform:scale(.96)} --.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} --.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} --/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ --.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} --.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --`; --function BrandMark({ size = 24 }) { -- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); --} --function BrandName() { -- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); ++ } ++ tops.current = { layout, map: next }; ++ }, [layout, key]); ++} ++var NodeActions = class extends import_react18.Component { ++ constructor(props) { ++ super(props); ++ this.state = { failed: false }; ++ } ++ static getDerivedStateFromError() { ++ return { failed: true }; ++ } ++ componentDidCatch(error) { ++ console.error("flybot: nodeActions failed", error); ++ } ++ render() { ++ return this.state.failed ? null : (0, import_react18.createElement)(this.props.component, { bot: this.props.bot }); ++ } ++}; ++function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { ++ const { bot, depth, children } = node; ++ const closed = collapsed[bot.id] === true; ++ return (0, import_react18.createElement)( ++ import_react18.Fragment, + null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null ++ (0, import_react18.createElement)( ++ "div", ++ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, ++ children.length ? (0, import_react18.createElement)( ++ "button", ++ { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, ++ (0, import_react18.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") ++ ) : (0, import_react18.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), ++ (0, import_react18.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), ++ typeof actions === "function" ? (0, import_react18.createElement)(NodeActions, { component: actions, bot }) : null ++ ), ++ closed ? null : children.map((child) => (0, import_react18.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) + ); + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); -} --var SIDEBAR_CSS = ` --.bt-side-top .bt-brand{cursor:default;padding-left:6px} --.bt-side-top .bt-brand:hover{background:none} --.bt-brand .bt-mark{flex:none} --.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} --.bt-rail .bt-row{position:relative} --.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} --.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} --[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} --.bt-foot-account{order:-1;flex:none;display:inline-flex} --[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} -- --/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a -- loop while the install runs. */ --.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-round.bt-update svg{width:17px;height:17px} --.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} --@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} --.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} --@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} --.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} --.bt-update-title{font-size:14px;line-height:20px;font-weight:600} --.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} --.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} --.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} --.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} --.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} --.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} --.bt-update-go:hover:not(:disabled){opacity:.9} --.bt-update-go:active:not(:disabled){transform:scale(.96)} --.bt-update-go:disabled{opacity:.6;cursor:default} --.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} --@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} -- --/* The DSH Agent row and its expanding Session list. The black whale is the -- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ --.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} --@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} --.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-agent-sub[data-open]{grid-template-rows:1fr} --.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} --.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} --.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} --.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} --.bt-agent-new{color:var(--bt-ink-3)} --.bt-agent-new svg{width:14px;height:14px;flex:none} --.bt-agent-edit{padding:3px 10px 3px 4px} --.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} --.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} --@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} --@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} --`; -- --// src/client/header.js --var import_react9 = require("react"); --var import_react_dom4 = require("react-dom"); --var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); --var MEMORY_NOTICE_MS = 3200; --var noNews = (select) => select({}); --function useMemoryNotice(ids, useMemoryNews) { -- const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); -- const [shown, setShown] = (0, import_react9.useState)(false); -- (0, import_react9.useEffect)(() => { -- const left = seen + MEMORY_NOTICE_MS - Date.now(); -- setShown(seen > 0 && left > 0); -- if (seen === 0 || left <= 0) return void 0; -- const timer = setTimeout(() => setShown(false), left); -- return () => clearTimeout(timer); -- }, [seen]); -- return shown; -+ tops.current = { layout, map: next }; -+ }, [layout, key]); - } --function MemoryNotice({ shown }) { -- const inner = (0, import_react9.useRef)(null); -- const [width, setWidth] = (0, import_react9.useState)(0); -- const label = t("Memory updated"); -- (0, import_react9.useLayoutEffect)(() => { -- if (shown) setWidth(inner.current?.scrollWidth ?? 0); -- }, [label, shown]); -- return (0, import_react9.createElement)( -- "span", -- { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, -- (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) -+var NodeActions = class extends import_react18.Component { -+ constructor(props) { -+ super(props); -+ this.state = { failed: false }; -+ } -+ static getDerivedStateFromError() { -+ return { failed: true }; -+ } -+ componentDidCatch(error) { -+ console.error("flybot: nodeActions failed", error); -+ } -+ render() { -+ return this.state.failed ? null : (0, import_react18.createElement)(this.props.component, { bot: this.props.bot }); -+ } -+}; -+function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { -+ const { bot, depth, children } = node; -+ const closed = collapsed[bot.id] === true; -+ return (0, import_react18.createElement)( -+ import_react18.Fragment, -+ null, -+ (0, import_react18.createElement)( -+ "div", -+ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, -+ children.length ? (0, import_react18.createElement)( -+ "button", -+ { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, -+ (0, import_react18.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") -+ ) : (0, import_react18.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), -+ (0, import_react18.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), -+ typeof actions === "function" ? (0, import_react18.createElement)(NodeActions, { component: actions, bot }) : null -+ ), -+ closed ? null : children.map((child) => (0, import_react18.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) - ); - } --function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { +-var PHRASES = { +- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] +-}; +-var TOOL_PHRASES = [ +- [/web_?search|search_?web/i, "Searching online"], +- [/fetch|browse|browser|web/i, "Reading a page"], +- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], +- [/^(read|view|cat)\b|read_/i, "Opening a file"], +- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], +- [/write|edit|patch|apply|replace/i, "Writing changes"], +- [/image|photo|draw/i, "Working on an image"] +-]; +-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; +-function toolActivity(name, arg, findBot, self) { +- switch (name) { +- case "ask_user": +- return null; +- case "request_secret": +- return null; +- case "list_secrets": +- return { label: t("Checking the keys"), state: "searching" }; +- case "ask_user_question": +- return { label: t("Waiting for your answer"), state: "alert" }; +- case "group_relay": +- return { relay: true, state: "orbit" }; +- case "message_bot": { +- const target = findBot(arg("to")); +- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; +- } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +- } +- case "create_bot": +- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; +function TeamSidebar(props) { + const { wide, useRoster, useUi, useSessions, actions } = props; + const listRef = (0, import_react18.useRef)(null); - const roster = useRoster((value) => value); -- const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); -- const firstTurns = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.slice(0, 8).map((entry) => entry.turn).join(",") : ""; -- }); -- const hiddenTurns = roster.exchangeTurns?.[sessionId]; -- const visit = useUi((state) => state.visits?.[sessionId]); -- const opening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); -- return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++ const roster = useRoster((value) => value); + const markedUnread = useUi((value) => value.unread); + props.useRegistry((value) => value); + const updated = useSessions((list) => { + const stamps = {}; + for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; + return JSON.stringify(stamps); - }); -- (0, import_react9.useEffect)(() => { -- if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); -- else document.body.style.removeProperty("--bt-first-sep"); -- return () => document.body.style.removeProperty("--bt-first-sep"); -- }, [opening]); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const owner = roster.byId[sessionId]?.id ?? sessionId; -- const doing = useActivity((map) => map[owner]); -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const detailsOpen = useUi((state) => state.details === sessionId); -- const name = bot?.name ?? room2?.name ?? ""; -- const useNews = useMemoryNews ?? noNews; -- const memoryIds = bot ? [bot.id] : room2?.members ?? []; -- const notice = useMemoryNotice(memoryIds, useNews); -- const changed = useNews((map) => latestMemoryChange(map, memoryIds)); -- (0, import_react9.useEffect)(() => { -- document.body.style.setProperty("--bt-placeholder", JSON.stringify(name ? t("Message {name}", { name }) : t("Message"))); -- }, [name, t("Message")]); -- (0, import_react9.useEffect)(() => { -- actions.beginVisit(sessionId); -- return () => actions.endVisit(sessionId); -- }, [sessionId]); -- (0, import_react9.useEffect)(() => { -- if (room2) document.body.dataset.btRoom = ""; -- else delete document.body.dataset.btRoom; -- return () => { -- delete document.body.dataset.btRoom; -- }; -- }, [Boolean(room2)]); -- const memoryNotice = notice && changed !== null && roster.memory !== false; -- return (0, import_react9.createElement)( ++ }); + const order = (0, import_react18.useMemo)(() => { + const stamps = JSON.parse(updated); + const entries2 = [ @@ -13567,7 +12399,36 @@ diff --git a/client.js b/client.js + className: "bt-read-only-reload", + onClick: () => { + void Promise.resolve(actions.reloadTeam?.()).catch((error) => actions.reportError(error, "reload")); -+ } + } +- 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("On it"); +-} +-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]); + }, t("Reload")); + const structure = useSidebarStructure(); + const [collapsed, setCollapsed] = (0, import_react18.useState)({}); @@ -13585,22 +12446,7 @@ diff --git a/client.js b/client.js + actions.openPalette(event.key); + }; + return (0, import_react18.createElement)( - "div", -- { className: "bt-head" }, -- (0, import_react9.createElement)( -- "button", -- { -- type: "button", -- className: "bt-pill", -- "aria-expanded": detailsOpen, -- "data-memory": memoryNotice ? "updated" : void 0, -- "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed]?.name ?? name }) : t("View conversation details"), -- onClick: () => memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId) -- }, -- bot ? (0, import_react9.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -- (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), -- (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), -- (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) ++ "div", + { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, + wide ? (0, import_react18.createElement)( + "div", @@ -13614,10 +12460,7 @@ diff --git a/client.js b/client.js + null, + (0, import_react18.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), + (0, import_react18.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react18.createElement)(PlusIcon)) - ), -- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), -- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) ++ ), + wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + wide && readOnlySaid ? readOnlyReload : null, + !wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, @@ -13632,43 +12475,23 @@ diff --git a/client.js b/client.js + order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react18.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react18.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react18.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), + wide ? (0, import_react18.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react18.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null + ) - ); ++ ); } --function useTranscriptView() { -- const [view, setView] = (0, import_react9.useState)(null); -- (0, import_react9.useEffect)(() => { -- let frame = 0; -- let watched = null; -- const observer = new MutationObserver(() => schedule()); -- const measure = () => { -- frame = 0; -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (scroller !== watched) { -- observer.disconnect(); -- watched = scroller; -- if (scroller) observer.observe(scroller, { childList: true, subtree: true }); -- } -- if (!scroller) { -- setView(null); -- return; -- } -- const box = scroller.getBoundingClientRect(); -- const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; -- const sep = scroller.querySelector(".bt-new-sep"); -- let divider = null; -- if (sep) { -- const at = sep.getBoundingClientRect(); -- divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; -- } -- const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; -- const next = { -- unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, -- divider, -- center: Math.round(box.left + box.width / 2), -- top: Math.round(headerBottom + 8), -- bottom: Math.round(window.innerHeight - box.bottom + composer + 8) -- }; -- setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +- if (done) return void 0; +- let stop = false; +- let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +function UpdateButton({ useUpdate, actions }) { + const update = useUpdate((value) => value); + const [open, setOpen] = (0, import_react18.useState)(false); @@ -13681,31 +12504,74 @@ diff --git a/client.js b/client.js + const onDown = (event) => { + if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; + setOpen(false); - }; -- const schedule = () => { -- if (!frame) frame = requestAnimationFrame(measure); ++ }; + const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); + setOpen(false); + button.current?.focus(); -+ } + } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); }; -- document.addEventListener("scroll", schedule, true); -- window.addEventListener("resize", schedule); -- schedule(); +- void tick(); + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- document.removeEventListener("scroll", schedule, true); -- window.removeEventListener("resize", schedule); +- stop = true; +- clearTimeout(timer); + window.removeEventListener("mousedown", onDown); + window.removeEventListener("keydown", onKey, true); }; -- }, []); -- return view; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { +- try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; +- } +- } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; +- const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-group-replies" }, +- replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); +- } +- if (reply.kind === "failed") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, +- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) +- ); +- } +- if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) +- ); +- } +- if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); +- } +- if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); +- } +- if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); +- } +- if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( + }, [open]); + const phase = update?.phase ?? "idle"; + const dsh = update?.dsh; @@ -13754,7 +12620,25 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), + message ? (0, import_react18.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react18.createElement)( -+ "div", + "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-update-actions" }, + update.notesUrl ? (0, import_react18.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react18.createElement)("button", { @@ -13766,7 +12650,12 @@ diff --git a/client.js b/client.js + ) + ) : null, + dshRow ? (0, import_react18.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-update-dsh" }, + (0, import_react18.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react18.createElement)("div", { className: "bt-update-text" }, dshText), @@ -13784,19 +12673,22 @@ diff --git a/client.js b/client.js + ) + ) : null + ), document.body) : null -+ ); + ); } --function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { -- const replies = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const unread = useTurnClock((state) => { -- if (typeof visit?.seen !== "number") return 0; -- let count = 0; -- for (const entry of Object.values(state[sessionId] ?? {})) { -- if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; +-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +-function callTree(root) { +- const calls = []; +- const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; +- if (seen.has(id)) return; +- seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); +- }; +- visit(root); +- return calls; +function ConnectApps({ wide, actions }) { + if (!wide) return null; + return (0, import_react18.createElement)( @@ -13809,7 +12701,15 @@ diff --git a/client.js b/client.js + t("Connect apps") + ) + ); -+} + } +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +function YouAvatar() { + return (0, import_react18.createElement)( + "span", @@ -13817,7 +12717,76 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react18.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react18.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + ); -+} + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +- const self = roster.byId[sessionId]; +- switch (tool) { +- case "message_bot": { +- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "stop_bot": { +- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "create_bot": { +- const created = findBot(arg("name")); +- if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); +- } +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; +- } +- case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); +- case "create_group": +- case "update_group": +- case "delete_group": +- case "post_to_group": { +- const verbs = { +- create_group: ["Created group", "Couldn't create the group"], +- update_group: ["Updated group", "Couldn't update the group"], +- delete_group: ["Deleted group", "Couldn't delete the group"], +- post_to_group: ["Posted in", "Couldn't post in the group"] +- }[tool]; +- if (!done) return null; +- const result = contentText(call2.content); +- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); +- const name = named[2]; +- const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); +- } +- case "ask_user": { +- const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- } +- case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); +- case "ask_user_question": { +- if (!done) return null; +- let parsed = null; +- try { +- parsed = JSON.parse(contentText(call2.content)); +- } catch { +- parsed = null; +- } +- const answer = parsed?.answers?.[0]; +- const question = argValue("questions")?.[0]; +- if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- } +- case "read_group_chat": { +- if (!done) return null; +- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +var openShellSettings = null; +var openShellOnboarding = null; +var shellShortcuts = null; @@ -13833,53 +12802,49 @@ diff --git a/client.js b/client.js + openShellSettings(); + return; } -- return count; +- 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 desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); + if (desktop) openThroughMenu(desktop); + else pressSettingsShortcut(); - }); -- const view = useTranscriptView(); -- const unpinned = view?.unpinned === true; -- const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); -- const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false }); -- (0, import_react9.useEffect)(() => { -- setRelease({ base: unpinned ? replies : null, dismissed: false }); -- }, [unpinned]); -- (0, import_react9.useEffect)(() => { -- if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); -- }, [view?.divider]); -- const fresh = release.base === null ? 0 : replies - release.base; -- const showDown = unpinned && fresh > 0 && !release.dismissed; -- const showUp = view?.divider === "above" && !above.seen && !above.dismissed; -- (0, import_react9.useEffect)(() => { -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (!scroller) return void 0; -- scroller.toggleAttribute("data-bt-news", showDown); -- return () => scroller.removeAttribute("data-bt-news"); -- }, [showDown]); -- if (!view || !showDown && !showUp) return null; -- const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); -- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( -- "div", -- { className: "bt-news", "data-direction": direction, style }, -- (0, import_react9.createElement)( -- "button", -- { type: "button", className: "bt-news-jump", onClick: onJump }, -- (0, import_react9.createElement)("span", { className: "bt-news-ico" }, (0, import_react9.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -- label(count) -- ), -- (0, import_react9.createElement)("button", { -- type: "button", -- className: "bt-news-x", -- "aria-label": t("Dismiss new messages"), -- onClick: (event) => { -+} ++ }); + } +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +- const roster = useRoster((value) => value); +- const root = node.data.root; +- const turn = node.location?.turn?.turn ?? root.turn; +- const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); +- const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); +- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; +- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); +- if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +function openHarnessOnboarding(id) { + requestAnimationFrame(() => { + if (openShellOnboarding) openShellOnboarding(id); + else openHarnessSettings(); + }); -+} + } +- +-// src/client/activity-row.js +-var import_react7 = require("react"); +-var import_react_dom2 = require("react-dom"); +-var PHRASE_HOLD_MS = 800; +-var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +var MODELS_STEP_ID = "flybot-models"; +var MODELS_STEP_ORDER = 1e3; +function ModelsSettingsStep({ explicit, complete, openSection }) { @@ -13888,7 +12853,19 @@ diff --git a/client.js b/client.js + complete(); + }, []); + return null; -+} + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +- } +- let frame = 0; +- const find = () => { +- frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); +- setHost((prev) => prev === node ? prev : node); +function openHarnessModels() { + openHarnessOnboarding(MODELS_STEP_ID); +} @@ -13937,55 +12914,91 @@ diff --git a/client.js b/client.js + openShellSettings = openSettings; + return () => { + if (openShellSettings === openSettings) openShellSettings = null; -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(find); +- }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); + }, [openSettings]); + (0, import_react18.useEffect)(() => { + if (!openOnboarding) return void 0; + openShellOnboarding = openOnboarding; -+ return () => { -+ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; -+ }; + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); ++ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; + }; +- }, [active]); +- return host; +-} +-function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); +- check.current = replacing; +- (0, import_react7.useEffect)(() => { +- const seen = /* @__PURE__ */ new WeakSet(); +- const started = Date.now(); +- let column = null; +- let frame = 0; +- const settle = (event) => { +- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; +- delete event.currentTarget.dataset.btEnter; +- event.currentTarget.removeEventListener("animationend", settle); + }, [openOnboarding]); + (0, import_react18.useEffect)(() => { + if (!open) return void 0; + const onDown = (event) => { + if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; + setOpen(false); -+ }; + }; +- const scan = () => { +- frame = 0; +- const next = document.querySelector("[data-chat-flow]"); +- if (next !== column) { +- observer.disconnect(); +- column = next; +- if (column) observer.observe(column, { childList: true, subtree: true }); +- } +- if (!column) return; +- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; +- let last = -1; +- items.forEach((item, index) => { +- if (seen.has(item)) last = index; +- }); +- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); +- for (const item of fresh) seen.add(item); +- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; +- for (const item of fresh) { +- if (items.indexOf(item) < last) continue; +- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); +- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; +- item.addEventListener("animationend", settle); + const onKey = (event) => { + if (event.key === "Escape") { - event.stopPropagation(); -- onDismiss(); ++ event.stopPropagation(); + setOpen(false); + button.current?.focus(); } -- }, (0, import_react9.createElement)(CloseIcon)) -- ); -- const toLatest = () => { -- const scroller = document.querySelector("[data-conversation-scroll]"); -- const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); -- if (back) back.click(); -- else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); +- }); +- scan(); + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + window.removeEventListener("mousedown", onDown); + window.removeEventListener("keydown", onKey, true); -+ }; + }; +- }, []); + }, [open]); + const toggle = () => { + const rect = button.current?.getBoundingClientRect(); + if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); + setOpen((value) => !value); - }; -- const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); -- return (0, import_react_dom4.createPortal)( -- (0, import_react9.createElement)( -- import_react9.Fragment, -- null, -- showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, -- showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null ++ }; + const groups = [ + [ + [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -14005,8 +13018,7 @@ diff --git a/client.js b/client.js + "button", + { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, + (0, import_react18.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react18.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) - ), -- document.body ++ ), + open && box ? (0, import_react_dom4.createPortal)((0, import_react18.createElement)( + "div", + { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, @@ -14026,31 +13038,35 @@ diff --git a/client.js b/client.js + }, (0, import_react18.createElement)(Icon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + )) + ), document.body) : null - ); ++ ); } -- --// src/client/composer.js --var import_react10 = require("react"); --var import_react_dom5 = require("react-dom"); --var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; --var keepComposerFocus = (event) => event.preventDefault(); --function useDictation(onText) { -- const [listening, setListening] = (0, import_react10.useState)(false); -- const [note, setNote] = (0, import_react10.useState)(""); -- const recognition = (0, import_react10.useRef)(null); -- const latest = (0, import_react10.useRef)(onText); -- latest.current = onText; -- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); -- (0, import_react10.useEffect)(() => { -- if (!note) return void 0; -- const timer = setTimeout(() => setNote(""), 2600); -- return () => clearTimeout(timer); -- }, [note]); -- const start = ({ continuous = true, onEnd } = {}) => { -- const Api = speechApi(); -- if (!Api) { -- setNote(t("Dictation is not available in this browser")); -- return false; +-function 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(); +function useLandOnMainBot(roster, useSessions, actions) { + const mainView = useSessions((list) => { + const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); @@ -14064,146 +13080,242 @@ diff --git a/client.js b/client.js + if (bot !== void 0) { + lastBot.current = { botId: bot.id, sessionId: id }; + if (currentPart(bot) !== id) actions.landOn(bot.id); -+ return void 0; + return void 0; } -- const engine = new Api(); -- engine.lang = navigator.language || "zh-CN"; -- engine.interimResults = true; -- engine.continuous = continuous; -- engine.onresult = (event) => { -- let interim = ""; -- for (let index = event.resultIndex; index < event.results.length; index += 1) { -- const result = event.results[index]; -- const text = result[0]?.transcript ?? ""; -- if (result.isFinal) { -- if (text.trim()) latest.current(text, true); -- } else interim += text; -- } -- if (interim) latest.current(interim, false); -- }; -- engine.onerror = (event) => { -- if (event.error === "aborted" || event.error === "no-speech") return; -- setNote(event.error === "not-allowed" || event.error === "service-not-allowed" ? t("Microphone access is blocked") : t("Couldn't hear that")); -- }; -- engine.onend = () => { -- setListening(false); -- recognition.current = null; -- onEnd?.(); -- }; -- recognition.current = engine; -- try { -- engine.start(); -- setListening(true); -- return true; -- } catch { -- setNote(t("Couldn't start dictation")); -- return false; +- 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"); + if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { + lastBot.current = null; -+ return void 0; + return void 0; } -- }; -- const stop = () => recognition.current?.stop(); -- return { listening, note, start, stop }; +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); + const last = lastBot.current; + const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400); -+ return () => clearTimeout(timer); + 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); + }, [mainView, roster]); } --function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -- const dictation = useDictation((text, final) => { -- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); -- }); -- const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -- (0, import_react10.useEffect)(() => { -- if (!quote || !inputActions) return; -- actions.clearQuote(quote.token); -- const excerpt = quote.text.replace(/\s+/g, " ").trim(); -- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} - --`; -- inputActions.insertText(line, inputActions.captureInsertion()); -- const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); -- if (box instanceof HTMLElement) { -- box.focus(); -- const selection = window.getSelection(); -- selection?.selectAllChildren(box); -- selection?.collapseToEnd(); -- } -- }, [quote?.token]); -- if (sessionId === void 0) return null; -- const pluginMic = actions.speech() !== void 0; -- return (0, import_react10.createElement)( -- import_react10.Fragment, -- null, -- pluginMic ? null : (0, import_react10.createElement)("button", { -- type: "button", -- className: "bt-mic", -- "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -- "aria-pressed": dictation.listening, -- title: dictation.listening ? t("Stop dictation") : t("Dictate"), -- onMouseDown: keepComposerFocus, -- onClick: () => dictation.listening ? dictation.stop() : dictation.start() -- }, (0, import_react10.createElement)(MicIcon)), -- (0, import_react10.createElement)("button", { -- type: "button", -- className: "bt-voice", -- "aria-label": t("Start voice chat"), -- title: t("Voice chat"), -- onMouseDown: keepComposerFocus, -- onClick: () => actions.openVoice(sessionId) -- }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), -- dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null +-// src/client/sidebar.js +-var import_react8 = require("react"); +-var import_react_dom3 = require("react-dom"); +-var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); +-function useMarkedUnread(id, active, marked, actions) { +- (0, import_react8.useEffect)(() => { +- if (active && marked) actions.clearUnread(id); +- }, [active, marked]); +function FooterAccount(props) { + const seated = props.useUi((value) => value.accountSeated === true); + if (seated) return null; + return (0, import_react18.createElement)("span", { className: "bt-foot-account" }, (0, import_react18.createElement)(AccountLauncher, { ...props, seat: false })); -+} -+function BotReturn({ wide, useSurface, actions }) { -+ const mode = useSurface((value) => value.mode); -+ if (mode !== "agent") return null; -+ return (0, import_react18.createElement)( -+ "button", -+ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, -+ (0, import_react18.createElement)(FlyBotMark, { size: 20 }), -+ wide ? (0, import_react18.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null - ); } --function menuIcons(roster) { -- const marks = /* @__PURE__ */ new Map(); -- return (row) => { -- if (!row.bot) return PeopleIcon; -- const id = row.bot.id; -- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); -- return marks.get(id); -- }; -+var RETURN_CSS = ` -+.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} -+.bt-return:hover{background:rgba(127,127,127,.14)} -+.bt-return:active{transform:scale(.96)} -+.bt-return .bt-mark,.bt-return .bt-flybot-mark{display:inline-flex;flex:none;line-height:0} -+.bt-return .bt-mark svg,.bt-return .bt-flybot-mark{width:20px;height:20px;overflow:visible} -+/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ -+.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} -+.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+`; -+function BrandMark({ size = 24 }) { -+ return (0, import_react18.createElement)("span", { className: "bt-brand-mark" }, (0, import_react18.createElement)(FlyBotMark, { size })); -+} -+function BrandName() { -+ return (0, import_react18.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); -+} -+var SIDEBAR_CSS = ` -+.bt-side-top .bt-brand{cursor:default;padding-left:6px} -+.bt-side-top .bt-brand:hover{background:none} -+.bt-brand .bt-mark{flex:none} -+.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} -+.bt-rail .bt-row{position:relative} -+.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} -+.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} -+.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} -+.bt-read-only-reload:hover{background:var(--bt-hover)} +-var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); +-var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; +-var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; +-var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { +- const [id, tool] = key.split("|"); +- return { id, tool: tool || void 0 }; +-}); +-var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); +-function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { +- const id = bot.id; +- const part = currentPart(bot); +- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); +- const running = useSessionStatus((map) => map.get(part)?.running === true); +- const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); +- const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; +- useMarkedUnread(id, active, marked, actions); +- const main = isMainOf(roster, id); +- const tile = roster.mainBotId === id; +- const approval = useApproval(useWork, (entry) => entry.botId === id); +- const alert = roster.questions?.[part] !== void 0 || approval; +- const doing = useActivity((map) => map[id]); +- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); +- const away = !running && groups.length > 0; +- const state = botState(running || away, doing, alert); +- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; +- if (bot.hidden && !active && !main) return null; +- const onContextMenu = (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); +- }; +- const pinned = bot.pinned === true && !tile; +- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); +- if (tile && wide) { +- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-tile", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: preview || bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu +- }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), +- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), +- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null +- )); +- } +- return (0, import_react8.createElement)( ++function BotReturn({ wide, useSurface, actions }) { ++ const mode = useSurface((value) => value.mode); ++ if (mode !== "agent") return null; ++ return (0, import_react18.createElement)( + "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: wide ? void 0 : bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu +- }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, ++ (0, import_react18.createElement)(FlyBotMark, { size: 20 }), ++ wide ? (0, import_react18.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null + ); + } +-function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { +- const id = room2.id; +- const fallback = useSessions((list) => previewOf(list, id)); +- const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; +- const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); +- const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; +- useMarkedUnread(id, active, marked, actions); +- const approval = useApproval(useWork, (entry) => entry.roomId === id); +- const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); +- const first = roster.byId[members[0]?.id]; +- const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; +- if (room2.hidden && !active) return null; +- const pinned = room2.pinned === true; +- const label = [room2.name, pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", approval ? t("Waiting for your approval") : ""].filter(Boolean).join(", "); +- return (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-room-id": id, +- onClick: () => actions.openSession(id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); +- } +- }, +- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null +- ); ++var RETURN_CSS = ` ++.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} ++.bt-return:hover{background:rgba(127,127,127,.14)} ++.bt-return:active{transform:scale(.96)} ++.bt-return .bt-mark,.bt-return .bt-flybot-mark{display:inline-flex;flex:none;line-height:0} ++.bt-return .bt-mark svg,.bt-return .bt-flybot-mark{width:20px;height:20px;overflow:visible} ++/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ ++.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} ++.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++`; ++function BrandMark({ size = 24 }) { ++ return (0, import_react18.createElement)("span", { className: "bt-brand-mark" }, (0, import_react18.createElement)(FlyBotMark, { size })); ++} ++function BrandName() { ++ return (0, import_react18.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); ++} ++var SIDEBAR_CSS = ` ++.bt-side-top .bt-brand{cursor:default;padding-left:6px} ++.bt-side-top .bt-brand:hover{background:none} ++.bt-brand .bt-mark{flex:none} ++.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} ++.bt-rail .bt-row{position:relative} ++.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} ++.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} ++.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} ++.bt-read-only-reload:hover{background:var(--bt-hover)} +.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} +[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} @@ -14275,26 +13387,76 @@ diff --git a/client.js b/client.js +function busyAckPending(hostAck) { + return busyAckText(hostAck) === ""; } --function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { -- const roster = useRoster((value) => value); -- const [open, setOpen] = (0, import_react10.useState)(null); -- const [shown, leaving] = useLinger(open, 120); -- const button = (0, import_react10.useRef)(null); -- const menu = (0, import_react10.useRef)(null); -- (0, import_react10.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); +-function AgentSessionRow({ session, agent, actions, renaming, index }) { +- const input = (0, import_react8.useRef)(null); +- (0, import_react8.useLayoutEffect)(() => { +- if (!renaming) return; +- input.current?.focus(); +- input.current?.select(); +- }, [renaming]); +- if (renaming) { +- const commit = (keep) => { +- const value = input.current?.value ?? ""; +- if (!keep || value.trim() === "" || value.trim() === session.title) { +- actions.endAgentRename(); +- return; +- } +- void (async () => { +- try { +- await actions.renameAgentSession(agent.id, session.id, value); +- actions.endAgentRename(); +- } catch (error) { +- actions.reportError(error, "agent-rename"); +- } +- })(); - }; -- const onKey = (event) => { -- if (event.key === "Escape") { +- return (0, import_react8.createElement)( +- "div", +- { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, +- (0, import_react8.createElement)("input", { +- ref: input, +- defaultValue: session.title, +- "aria-label": t("Rename"), +- onKeyDown: (event) => { +- if (event.nativeEvent.isComposing) return; +- if (event.key === "Enter") commit(true); +- else if (event.key === "Escape") commit(false); +- }, +- onBlur: () => commit(true) +- }) +- ); +- } +- return (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-agent-sub-row", +- "aria-current": session.active ? "page" : void 0, +- style: { "--i": index }, +- onClick: () => actions.openAgentSession(session.id), +- onContextMenu: (event) => { +- event.preventDefault(); - event.stopPropagation(); -- setOpen(null); +- actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); - } +- }, +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") +- ); +function pendingBusyPlacement(submissions) { + if (!Array.isArray(submissions)) return false; + return submissions.some((item) => item?.placement === "queued" || item?.placement === "steering"); -+} + } +-function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { +- const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { +- const row = list.byId[id]; +- return { +- id, +- // A blank Session's title is the workspace folder name; show the label instead. +- title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", +- at: row?.updatedAt ?? 0, +- running: row?.running === true, +- active: (row?.retainedBy?.mainView ?? 0) > 0 +function busyAckVisible({ hostAck, pendingBusy, streamingText }) { + if (streamingText) return false; + return busyAckText(hostAck) !== "" || pendingBusy === true; @@ -14326,29 +13488,37 @@ diff --git a/client.js b/client.js + const node = document.querySelector(RUNNING_HOST); + setHost((prev) => prev === node ? prev : node); }; -- window.addEventListener("mousedown", onDown, true); -- window.addEventListener("keydown", onKey, true); +- }).sort((a, b) => b.at - a.at))); +- const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ +- id, +- running: map.get(id)?.running === true, +- unread: map.get(id)?.completionUnread === true +- })))); +- const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); +- const sessions = JSON.parse(info).map((session) => ({ +- ...session, +- running: statuses[session.id]?.running ?? session.running, +- unread: statuses[session.id]?.unread ?? false, +- title: session.title === "" ? t("New session") : session.title +- })); +- const renaming = useUi((value) => value.renameAgent); +- const active = sessions.some((session) => session.active); +- const running = sessions.some((session) => session.running); +- const unread = sessions.some((session) => session.unread) || marked && !active; +- useMarkedUnread(agent.id, active, marked, actions); +- const preview = sessions[0]?.title ?? t("New session"); +- const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); +- const open = active && wide; +- return (0, import_react8.createElement)( + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(find); + }); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + find(); - return () => { -- window.removeEventListener("mousedown", onDown, true); -- window.removeEventListener("keydown", onKey, true); ++ return () => { + cancelAnimationFrame(frame); + observer.disconnect(); - }; -- }, [open]); -- const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; -- if (sessionId === void 0 || target === void 0) return null; -- const toggle = () => { -- if (open) { -- setOpen(null); -- return; -- } -- const rect = button.current.getBoundingClientRect(); -- setOpen({ left: rect.left, bottom: window.innerHeight - rect.top + 8 }); ++ }; + }, [active]); + return host; +} @@ -14368,21 +13538,66 @@ diff --git a/client.js b/client.js + const text2 = shown.current?.text ?? ""; + if (!visible || !host || !shown.current || !bot) return null; + return (0, import_react_dom5.createPortal)((0, import_react19.createElement)( -+ "div", -+ { -+ className: "bt-busy-ack", -+ "data-flybot": "busy-ack", -+ role: "status", + "div", +- { className: "bt-agent" }, +- (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-agent-id": agent.id, +- title: wide ? void 0 : "DSH Agent", +- onClick: () => actions.openAgent(agent.id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); +- } +- }, +- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), +- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null +- ), +- (0, import_react8.createElement)( ++ { ++ className: "bt-busy-ack", ++ "data-flybot": "busy-ack", ++ role: "status", + "aria-live": "polite", + "aria-busy": text2 ? void 0 : "true", + "data-pending": text2 ? void 0 : "" + }, + (0, import_react19.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }), + (0, import_react19.createElement)( -+ "div", + "div", +- { className: "bt-agent-sub", "data-open": open || void 0 }, +- (0, import_react8.createElement)( + { className: "bt-astack" }, + (0, import_react19.createElement)( -+ "div", + "div", +- { className: "bt-agent-sub-in" }, +- (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { +- void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); +- } }, +- (0, import_react8.createElement)(PlusIcon), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) +- ), +- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { +- key: session.id, +- session, +- agent, +- actions, +- index: index + 1, +- renaming: renaming === session.id +- })) + { className: "bt-msg" }, + (0, import_react19.createElement)( + "div", @@ -14395,10 +13610,27 @@ diff --git a/client.js b/client.js + (0, import_react19.createElement)("span", null) + ) + ) -+ ) -+ ) + ) + ) +- ); + ), host); -+} + } +-var GLIDE = springEasing(1e3, 63); +-function useListGlide(listRef, layout, key) { +- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); +- (0, import_react8.useLayoutEffect)(() => { +- const list = listRef.current; +- if (!list) return; +- const next = /* @__PURE__ */ new Map(); +- const still = reducedMotion() || tops.current.layout !== layout; +- for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { +- const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; +- const top = row.offsetTop; +- next.set(id, top); +- const before = tops.current.map.get(id); +- if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { +- row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); +- } +var BUSY_ACK_CSS = ` +.bt-busy-ack{display:flex;align-items:flex-end;gap:8px;width:100%;margin-top:var(--dsh-chat-flow-gap,12px);margin-bottom:4px} +.bt-busy-ack .bt-astack{align-items:flex-start} @@ -14569,10 +13801,7 @@ diff --git a/client.js b/client.js + title: text(file.title), + sha256: text(file.sha256), + shortHash: text(file.sha256).slice(0, 12) - }; -- const attach = () => { -- setOpen(null); -- button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); ++ }; +}; +var meetingOf = (meeting, status) => { + if (meeting === null || typeof meeting !== "object") return null; @@ -14596,10 +13825,7 @@ diff --git a/client.js b/client.js + // #1861: the host's participants with who each position speaks for, so the row can say a + // Project Chief or an agent held it for someone rather than for themselves. + participants: Array.isArray(meeting.participants) ? meeting.participants.map((participant) => ({ name: text(participant?.name), speaksFor: text(participant?.speaksFor) || null })).filter((participant) => participant.name !== "") : [] - }; -- const schedule = () => { -- setOpen(null); -- actions.openSettings("schedules", target); ++ }; +}; +var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; +function projectBoardRecord({ decisions, meetings } = {}) { @@ -14632,12 +13858,41 @@ diff --git a/client.js b/client.js + pendingHidden: hiddenOf(pendingTotal, pending.length), + empty: decided.length === 0 && pending.length === 0, + emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." -+ } - }; -- const reference = () => { -- setOpen(null); -- actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); -+} + } +- tops.current = { layout, map: next }; +- }, [layout, key]); ++ }; + } +-function TeamSidebar(props) { +- const { wide, useRoster, useUi, useSessions, actions } = props; +- const listRef = (0, import_react8.useRef)(null); +- const roster = useRoster((value) => value); +- const markedUnread = useUi((value) => value.unread); +- props.useRegistry((value) => value); +- const updated = useSessions((list) => { +- const stamps = {}; +- for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; +- return JSON.stringify(stamps); +- }); +- const order = (0, import_react8.useMemo)(() => { +- const stamps = JSON.parse(updated); +- const entries2 = [ +- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), +- ...roster.rooms.map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), +- // The Agent sorts by its most recent Session, falling back to when it was added. +- ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) +- ]; +- return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || b.at - a.at); +- }, [roster, updated]); +- const main = roster.byId[roster.mainBotId]; +- useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); +- useLandOnMainBot(roster, useSessions, actions); +- const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; +- const typeToSearch = (event) => { +- if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; +- if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; +- event.preventDefault(); +- actions.openPalette(event.key); +function projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics } = {}) { + const isRow = (row) => row !== null && typeof row === "object" && text(row.id) !== ""; + const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); @@ -14671,35 +13926,7 @@ diff --git a/client.js b/client.js + board: boardOfRow(row, task) + }; }; -- const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); -- return (0, import_react10.createElement)( -- import_react10.Fragment, -- null, -- (0, import_react10.createElement)("button", { -- ref: button, -- type: "button", -- className: "bt-plus", -- "aria-label": t("Add"), -- title: t("Add"), -- "aria-haspopup": "menu", -- "aria-expanded": Boolean(open), -- onMouseDown: keepComposerFocus, -- onClick: toggle -- }, (0, import_react10.createElement)(PlusIcon)), -- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( -- "div", -- { -- ref: menu, -- className: "bt-menu bt-menu-up bt-plus-menu", -- role: "menu", -- "aria-label": t("Add"), -- style: { left: shown.left, bottom: shown.bottom }, -- "data-leaving": leaving || void 0 -- }, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -- ), document.body) : null +- return (0, import_react8.createElement)( + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], @@ -14741,7 +13968,20 @@ diff --git a/client.js b/client.js +function projectRowNode(row, onOpen) { + const open = row.board === null ? void 0 : () => onOpen?.(row.board); + return (0, import_react20.createElement)( -+ "div", + "div", +- { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, +- wide ? (0, import_react8.createElement)( +- "div", +- { className: "bt-side-top" }, +- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) +- ) : (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) + { + key: row.key, + className: "bt-project-row", @@ -14763,7 +14003,14 @@ diff --git a/client.js b/client.js + // The audit text the row already carries (#1730): shown inside the card detail, never a + // second read. A row without one renders exactly as before. + row.audit === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-audit" }, row.audit) -+ ), + ), +- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, +- (0, import_react8.createElement)( +- "div", +- { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, +- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), +- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) +- ) + // "Who is doing" is the claim, never the assignee label; a card the Assistant speaks for + // says so, so the three identities (Bot, Assistant, user) never read as one. + row.holder === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), @@ -14772,30 +14019,31 @@ diff --git a/client.js b/client.js + row.runId === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) ); } -- --// src/client/at-menu.js --var BOTS_SOURCE = "ds-bot"; --var CHATS_SOURCE = "ds-bot-chats"; --var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; --var OTHER_CHATS_MAX = 8; --var conversationOf = (roster, sessionId) => { -- if (!roster.ready) return null; -- const room2 = roster.roomsById[sessionId]; -- if (room2) return { room: room2 }; -- const self = roster.byId[sessionId]; -- return self ? { self } : null; --}; --function mentionRows(roster, sessionId, query = "") { -- const here = conversationOf(roster, sessionId); -- if (here === null) return null; -- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); -- const needle = query.trim().toLowerCase(); -- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); -- const everyone = t("Everyone"); -- if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -- rows.push({ room: here.room, text: `@${everyone} ` }); -- } -- return rows; +-function UpdateButton({ useUpdate, actions }) { +- const update = useUpdate((value) => value); +- const [open, setOpen] = (0, import_react8.useState)(false); +- const [box, setBox] = (0, import_react8.useState)(null); +- const [error, setError] = (0, import_react8.useState)(""); +- const [dshError, setDshError] = (0, import_react8.useState)(""); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; +- setOpen(false); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +function ProjectBoardRows({ view, onOpen }) { + if (view.empty) return (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); + return view.groups.map((group) => (0, import_react20.createElement)( @@ -14804,14 +14052,7 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)("h4", null, t(group.label)), + group.rows.map((row) => projectRowNode(row, onOpen)) + )); - } --function chatRows(roster, list, sessionId) { -- const here = conversationOf(roster, sessionId); -- if (here === null) return []; -- const byId = list?.byId ?? {}; -- const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { -- const id = currentPart(bot); -- return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; ++} +function ProjectDoneGroup({ view, onOpen }) { + if (view.done.rows.length === 0) return null; + return (0, import_react20.createElement)( @@ -14865,14 +14106,27 @@ diff --git a/client.js b/client.js + lastActivityAt: Number.isFinite(row.lastActivityAt) ? row.lastActivityAt : null, + overdue: Number.isFinite(row.sinceMs) && row.sinceMs > RUNNING_OVERDUE_MS, + board: boardOfRow({ source: text(row.source) }, task) -+ }; - }); -- const team = /* @__PURE__ */ new Set([ -- ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), -- ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) -- ]); -- const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); -- return [...bots, ...others]; + }; +- }, [open]); +- const phase = update?.phase ?? "idle"; +- const dsh = update?.dsh; +- const dshPhase = dsh?.phase ?? "idle"; +- const dshRow = showsDshRow(dsh, update?.app); +- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; +- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; +- const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); +- setOpen((value) => !value); +- }; +- const start = () => { +- setError(""); +- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); +- }; +- const startDsh = () => { +- setDshError(""); +- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); ++ }); + return { + available: source?.ok === true, + reason: text(source?.reason) || null, @@ -14880,48 +14134,99 @@ diff --git a/client.js b/client.js + running, + empty: source === null, + emptyText: "No metrics read yet." -+ }; - } --function sessionMention(sessionId, label) { -- let binary = ""; -- for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); -- const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -- return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; + }; +- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); +- const message = error || (phase === "failed" ? update.error : ""); +- const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); +- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); +- return (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)("button", { +- ref: button, +- type: "button", +- className: "bt-round bt-update", +- "data-phase": cloudPhase, +- "aria-label": label, +- title: label, +- "aria-haspopup": "dialog", +- "aria-expanded": open, +- onClick: toggle +- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++} +function ProjectMetrics({ view }) { + return (0, import_react20.createElement)( + "section", + { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, + view.available || view.reason === null ? null : (0, import_react20.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), + (0, import_react20.createElement)( -+ "div", -+ { className: "bt-project-metric-cards" }, -+ view.cards.map((card) => (0, import_react20.createElement)( -+ "div", -+ { key: card.key, className: "bt-project-metric", "data-metric": card.key }, -+ (0, import_react20.createElement)("span", { className: "bt-project-metric-value" }, card.value), -+ (0, import_react20.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), -+ (0, import_react20.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) -+ )) -+ ) -+ ); + "div", +- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, +- update?.available || phase !== "idle" ? (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), +- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-update-actions" }, +- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, +- phase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: phase === "installing", +- onClick: start +- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null, +- dshRow ? (0, import_react8.createElement)( ++ { className: "bt-project-metric-cards" }, ++ view.cards.map((card) => (0, import_react20.createElement)( + "div", +- { className: "bt-update-dsh" }, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), +- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-update-actions" }, +- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { +- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: dshPhase === "installing", +- onClick: startDsh +- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null +- ), document.body) : null ++ { key: card.key, className: "bt-project-metric", "data-metric": card.key }, ++ (0, import_react20.createElement)("span", { className: "bt-project-metric-value" }, card.value), ++ (0, import_react20.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), ++ (0, import_react20.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) ++ )) ++ ) + ); } --function atSources({ roster, sessions, launched, iconFor }) { -- const bots = { -- trigger: "@", -- name: BOTS_SOURCE, -- order: -20, -- showGroupTitle: false, -- async candidates(session, { query }) { -- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ -- name: row.bot ? row.bot.name : t("Everyone"), -- ...row.bot?.role ? { description: row.bot.role } : {}, -- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, -- icon: iconFor(row), -- section: t("Bots"), -- value: row.text -- })); -- }, -- onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 +-function ConnectPlugins({ wide, actions }) { +- const tipId = (0, import_react8.useId)(); +- const [tip, setTip] = (0, import_react8.useState)(false); +- const timer = (0, import_react8.useRef)(0); +- const button = (0, import_react8.useRef)(null); +- const [box, setBox] = (0, import_react8.useState)(null); +- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); +- if (!wide) return null; +- const show = () => { +- clearTimeout(timer.current); +- timer.current = setTimeout(() => { +- const rect = button.current?.getBoundingClientRect(); +- if (!rect) return; +- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); +- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); +- setTip(true); +- }, 250); +function ProjectRunningTable({ view, onOpen }) { + if (view.running.length === 0) return (0, import_react20.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react20.createElement)("h4", null, t("Running tasks")), (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); + return (0, import_react20.createElement)( @@ -15007,50 +14312,62 @@ diff --git a/client.js b/client.js + const settle = (answer) => setOpen((current) => current !== null && current.path === path ? { path, answer } : current); + call4("project-decision", { id: projectId, path }).then(settle, (error) => settle({ ok: false, reason: String(error?.message ?? error) })); }; -- const chats = { -- trigger: "@", -- name: CHATS_SOURCE, -- order: -10, -- showGroupTitle: false, -- async candidates(session) { -- if (!launched(session.sessionId)) return []; -- return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ -- name: row.bot ? row.bot.name : row.label, -- ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, -- icon: row.bot ? iconFor(row) : "session", -- section: row.bot ? t("Bots") : t("Conversations"), -- value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) -- })); -- }, -- onPick({ candidate }) { -- const { sessionId, label } = JSON.parse(candidate.value); -- const mention = sessionMention(sessionId, label); -- return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; +- const hide = () => { +- clearTimeout(timer.current); +- setTip(false); + const decisionBody = (file) => { + if (open === null || open.path !== file.path) return null; + const answer = open.answer; + if (answer === null) return (0, import_react20.createElement)("div", { className: "bt-project-decision-text", "data-open-decision": file.path }, t("Reading the decision…")); + if (answer.ok !== true) return (0, import_react20.createElement)("div", { className: "bt-project-decision-text bt-project-decision-refusal", "data-open-decision": file.path, "data-refused": "true" }, text(answer.reason) || t("not listed")); + return (0, import_react20.createElement)("pre", { className: "bt-project-decision-text", "data-open-decision": file.path }, typeof answer.text === "string" ? answer.text : ""); -+ }; + }; +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-connect-wrap" }, +- (0, import_react8.createElement)( + const decisionRows = decisions.files.map((file) => (0, import_react20.createElement)( + "div", + { key: file.key, className: "bt-project-decision-wrap" }, + (0, import_react20.createElement)( -+ "button", -+ { -+ type: "button", + "button", + { +- ref: button, + type: "button", +- className: "bt-connect bt-connect-foot", +- "aria-describedby": tip ? tipId : void 0, +- onMouseEnter: show, +- onMouseLeave: hide, +- onFocus: show, +- onBlur: hide, +- onClick: () => actions.selectPanel("plugins") + className: "bt-project-decision", + "data-path": file.path, + "data-sha256": file.sha256, + "data-open": open !== null && open.path === file.path ? "true" : void 0, + "aria-expanded": open !== null && open.path === file.path ? "true" : "false", + onClick: () => toggle(file.path) -+ }, + }, +- (0, import_react8.createElement)(PlugIcon), +- t("Connect plugins"), +- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") + (0, import_react20.createElement)("span", { className: "bt-project-date" }, file.date), + (0, import_react20.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), + (0, import_react20.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) -+ ), + ), +- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( +- "span", +- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, +- t("Plugin compatibility is not fully verified.") +- ), document.body) : null +- ); +-} +-function YouAvatar() { +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-you-wrap" }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), +- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + decisionBody(file) + )); + const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); @@ -15088,8 +14405,7 @@ diff --git a/client.js b/client.js + "data-decisions": String(decisions.count), + ...decisions.total === null ? {} : { "data-decisions-total": String(decisions.total) }, + "data-meetings": String(meetings.count) - }, -- codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } ++ }, + (0, import_react20.createElement)("h4", null, t("Decisions")), + decisions.empty ? (0, import_react20.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, + moreLine(decisions.hidden, "decisions-more"), @@ -15099,8 +14415,15 @@ diff --git a/client.js b/client.js + meetings.pending.length === 0 ? null : (0, import_react20.createElement)("h5", null, t("Not decided")), + meetings.pending.length === 0 ? null : pendingRows, + moreLine(meetings.pendingHidden, "pending-more") -+ ); -+} + ); + } +-var openShellSettings = null; +-var openShellOnboarding = null; +-var shellShortcuts = null; +-function holdShellShortcuts(shortcuts) { +- shellShortcuts = shortcuts; +- return () => { +- if (shellShortcuts === shortcuts) shellShortcuts = null; +var botIdOf = (entry) => entry !== null && typeof entry === "object" ? text(entry.botId) : ""; +var idList = (value) => Array.isArray(value) ? value.map((id) => text(id)).filter((id) => id !== "") : []; +function projectTeamView(project) { @@ -15124,18 +14447,12 @@ diff --git a/client.js b/client.js + role: text(project?.role), + choices: idList(project?.choices) }; -- return [bots, chats]; } -- --// src/client/speech.js --var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; --var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); --async function speechCatalog(speech) { -- const result = await speech.catalog(); -- if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); -- const catalog = result.value; -- const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); -- return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; +-function openHarnessSettings() { +- requestAnimationFrame(() => { +- if (openShellSettings) { +- openShellSettings(); +- return; +var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react20.createElement)( + "div", + { @@ -15155,7 +14472,30 @@ diff --git a/client.js b/client.js + onOpenBot?.(botId); + } + } -+ } + } +- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); +- if (desktop) openThroughMenu(desktop); +- else pressSettingsShortcut(); +- }); +-} +-function openHarnessOnboarding(id) { +- requestAnimationFrame(() => { +- if (openShellOnboarding) openShellOnboarding(id); +- else openHarnessSettings(); +- }); +-} +-function openThroughMenu(trigger) { +- const before = new Set(document.querySelectorAll('[role="menu"]')); +- trigger.click(); +- let frames = 0; +- const look = () => { +- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); +- const item = menu?.querySelector('button[role="menuitem"]'); +- if (item) item.click(); +- else if (++frames < 30) requestAnimationFrame(look); +- else pressSettingsShortcut(); +- }; +- requestAnimationFrame(look); + }, + (0, import_react20.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), + botId === "" ? (0, import_react20.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react20.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`), @@ -15220,29 +14560,20 @@ diff --git a/client.js b/client.js + ) + ); } --function waveBytes(samples, rate = 16e3) { -- const bytes = new Uint8Array(44 + samples.length * 2); -- const view = new DataView(bytes.buffer); -- const ascii = (at, text) => { -- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); -- }; -- ascii(0, "RIFF"); -- view.setUint32(4, 36 + samples.length * 2, true); -- ascii(8, "WAVEfmt "); -- view.setUint32(16, 16, true); -- view.setUint16(20, 1, true); -- view.setUint16(22, 1, true); -- view.setUint32(24, rate, true); -- view.setUint32(28, rate * 2, true); -- view.setUint16(32, 2, true); -- view.setUint16(34, 16, true); -- ascii(36, "data"); -- view.setUint32(40, samples.length * 2, true); -- samples.forEach((sample, index) => { -- const value = Math.max(-1, Math.min(1, sample)); -- view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); -- }); -- return bytes; +-function pressSettingsShortcut() { +- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; +- if (!binding) return; +- const held = new Set(binding.modifiers); +- window.dispatchEvent(new KeyboardEvent("keydown", { +- code: binding.code, +- key: binding.code === "Comma" ? "," : "", +- bubbles: true, +- cancelable: true, +- ctrlKey: held.has("control"), +- altKey: held.has("alt"), +- shiftKey: held.has("shift"), +- metaKey: held.has("meta") +- })); +function dossierRowOf(team, botId) { + const id = text(botId); + if (id === "") return null; @@ -15293,10 +14624,43 @@ diff --git a/client.js b/client.js + ) + ); } --function base64Of(bytes) { -- let text = ""; -- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); -- return btoa(text); +-function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { +- const [open, setOpen] = (0, import_react8.useState)(false); +- (0, import_react8.useLayoutEffect)(() => { +- if (!seat) return void 0; +- actions.seatAccount(true); +- return () => actions.seatAccount(false); +- }, [seat]); +- const [box, setBox] = (0, import_react8.useState)(null); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (settingsOpen) setOpen(false); +- }, [settingsOpen]); +- (0, import_react8.useEffect)(() => { +- if (!openSettings) return void 0; +- openShellSettings = openSettings; +- return () => { +- if (openShellSettings === openSettings) openShellSettings = null; +- }; +- }, [openSettings]); +- (0, import_react8.useEffect)(() => { +- if (!openOnboarding) return void 0; +- openShellOnboarding = openOnboarding; +- return () => { +- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; +- }; +- }, [openOnboarding]); +- (0, import_react8.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; +- setOpen(false); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); +function projectAgentFor(bot, selfId) { + if (bot === null || typeof bot !== "object" || bot.remote !== void 0) return null; + const projectId = text(bot.projectId); @@ -15317,11 +14681,41 @@ diff --git a/client.js b/client.js + target = botIdOf(record?.project?.assistant); + } catch { + target = ""; -+ } + } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +- const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); +- setOpen((value) => !value); + if (target !== "") setAgentId(target); + } + if (target !== "") onOpen?.(target); -+ }; + }; +- const groups = [ +- [ +- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], +- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], +- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], +- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] +- ], +- [ +- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] +- ] +- ]; +- return (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)( +- "button", +- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + if (id === "") return null; + return (0, import_react20.createElement)("button", { + type: "button", @@ -15348,13 +14742,21 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)("span", null, [t("In flight"), ": ", (0, import_react20.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), + (0, import_react20.createElement)("span", null, [t("Blocked"), ": ", (0, import_react20.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), + (0, import_react20.createElement)("span", null, [t("Done"), ": ", (0, import_react20.createElement)("b", { key: "d" }, countText(view.counts.done))]) -+ ), + ), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + // #1749: both buttons open a panel and neither is hidden from a non-owner. The host refuses + // every write a non-owner attempts, so the refusal is an answer the reader sees, not a + // control that silently disappeared. + (0, import_react20.createElement)( -+ "div", -+ { className: "bt-project-actions" }, + "div", +- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, +- groups.map((group, index) => (0, import_react8.createElement)( +- import_react8.Fragment, +- { key: index }, +- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { +- key: label, ++ { className: "bt-project-actions" }, + (0, import_react20.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), + (0, import_react20.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) + ), @@ -15365,26 +14767,7 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)(ProjectDoneGroup, { view, onOpen }), + (0, import_react20.createElement)(ProjectBoardRecord, { record: view.record, projectId: view.projectId, call: call4 }) + ); - } --var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); --async function openCapture() { -- const devices = navigator.mediaDevices; -- if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); -- let stream; -- try { -- stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); -- } catch (error) { -- throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); -- } -- const audio = new AudioContext(); -- const analyser = audio.createAnalyser(); -- analyser.fftSize = 512; -- audio.createMediaStreamSource(stream).connect(analyser); -- const frame = new Float32Array(analyser.fftSize); -- const recorder = new MediaRecorder(stream); -- const chunks = []; -- recorder.ondataavailable = (event) => { -- if (event.data.size > 0) chunks.push(event.data); ++} +function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { + const save = request ?? call; + const [kind, setKind] = (0, import_react20.useState)("bot"); @@ -15406,16 +14789,7 @@ diff --git a/client.js b/client.js + setStatus(hostText(error?.message ?? String(error))); + return null; + } - }; -- recorder.start(); -- let closed = false; -- const close = () => { -- if (closed) return; -- closed = true; -- if (recorder.state !== "inactive") recorder.stop(); -- stream.getTracks().forEach((track) => track.stop()); -- void audio.close().catch(() => { -- }); ++ }; + return (0, import_react20.createElement)( + "div", + { className: "bt-project-panel", "data-panel": "project-settings" }, @@ -15461,16 +14835,20 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-project-row-actions" }, + (0, import_react20.createElement)("button", { -+ type: "button", + type: "button", +- role: "menuitem", + className: "bt-soft bt-soft-sm", + "data-action": "project-memory-save", + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. -+ onClick: () => { + onClick: () => { +- setOpen(false); +- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); + if (noteSaveDisabled) return; + run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")); -+ } + } +- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + }, t("Save memory")) + ) + ), @@ -15487,7 +14865,7 @@ diff --git a/client.js b/client.js + const add = (id) => { + const clean = text(id); + if (clean !== "" && !owners.includes(clean)) owners.push(clean); - }; ++ }; + add(project?.ownerUserId); + for (const id of Array.isArray(project?.ownerUserIds) ? project.ownerUserIds : []) add(id); + const boardMembers = []; @@ -15520,7 +14898,8 @@ diff --git a/client.js b/client.js + { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, + (0, import_react20.createElement)("span", null, `@${name}`), + (0, import_react20.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) -+ )) + )) +- ), document.body) : null + ), + owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, + (0, import_react20.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")), @@ -15529,8 +14908,21 @@ diff --git a/client.js b/client.js + { className: "bt-project-row-actions" }, + (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) -+ ); -+} + ); + } +-function useLandOnMainBot(roster, useSessions, actions) { +- const mainView = useSessions((list) => { +- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); +- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; +- }); +- const lastBot = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!roster.ready || !roster.mainBotId) return void 0; +- const [id, blank3] = mainView.split("|"); +- const bot = id === "" ? void 0 : roster.byId[id]; +- if (bot !== void 0) { +- lastBot.current = { botId: bot.id, sessionId: id }; +- if (currentPart(bot) !== id) actions.landOn(bot.id); +async function readProjectBoard(read, wanted, knownTeam) { + const listing = await read("projects"); + const projects = Array.isArray(listing?.projects) ? listing.projects : []; @@ -15553,20 +14945,7 @@ diff --git a/client.js b/client.js + const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null }; + const base = record?.project ?? project; + const withRole = { ...base, role: project.role ?? null, choices: Array.isArray(project.choices) ? project.choices : [] }; - return { -- level() { -- if (closed) return 0; -- analyser.getFloatTimeDomainData(frame); -- let sum = 0; -- for (const sample of frame) sum += sample * sample; -- return Math.min(1, Math.sqrt(sum / frame.length) * 4); -- }, -- cancel: close, -- async finish(maxSeconds) { -- if (closed || recorder.state === "inactive") { -- close(); -- return null; -- } ++ return { + project: withRole, + assistantMine: record?.assistantMine ?? null, + rows: board?.tasks ?? [], @@ -15617,8 +14996,11 @@ diff --git a/client.js b/client.js + if (typeof window === "undefined") return void 0; + if (!hash.startsWith("#project")) { + setState(null); -+ return void 0; -+ } + return void 0; + } +- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { +- lastBot.current = null; +- return void 0; + const wanted = hash.slice("#project".length).replace(/^=/, ""); + let gone = false; + team.current = void 0; @@ -15626,24 +15008,7 @@ diff --git a/client.js b/client.js + const loadBoard = () => readProjectBoard(read, wanted, team.current); + const refresh = async (first) => { + const gen = teamGen.current; - try { -- await new Promise((resolve) => { -- recorder.onstop = resolve; -- recorder.stop(); -- }); -- stream.getTracks().forEach((track) => track.stop()); -- const blob = new Blob(chunks, { type: recorder.mimeType }); -- if (blob.size === 0) return null; -- const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); -- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); -- const offline = new OfflineAudioContext(1, length, 16e3); -- const source = offline.createBufferSource(); -- source.buffer = decoded; -- source.connect(offline.destination); -- source.start(); -- return waveBytes((await offline.startRendering()).getChannelData(0)); -- } finally { -- close(); ++ try { + const next = await loadBoard(); + if (!gone) { + const newer = teamGen.current !== gen; @@ -15652,7 +15017,7 @@ diff --git a/client.js b/client.js + } + } catch { + if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); - } ++ } + }; + void refresh(true); + const timer = setInterval(() => { @@ -15677,7 +15042,103 @@ diff --git a/client.js b/client.js + setState((previous) => previous === null ? previous : { ...previous, team: answer }); + } catch { } - }; +- const last = lastBot.current; +- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); +- return () => clearTimeout(timer); +- }, [mainView, roster]); +-} +-function FooterAccount(props) { +- const seated = props.useUi((value) => value.accountSeated === true); +- if (seated) return null; +- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); +-} +-function BotReturn({ wide, useSurface, actions }) { +- const mode = useSurface((value) => value.mode); +- if (mode !== "agent") return null; +- return (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, +- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), +- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null +- ); +-} +-var RETURN_CSS = ` +-.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} +-.bt-return:hover{background:rgba(127,127,127,.14)} +-.bt-return:active{transform:scale(.96)} +-.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} +-.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} +-/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ +-.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} +-.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-`; +-function BrandMark({ size = 24 }) { +- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); +-} +-function BrandName() { +- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); +-} +-var SIDEBAR_CSS = ` +-.bt-side-top .bt-brand{cursor:default;padding-left:6px} +-.bt-side-top .bt-brand:hover{background:none} +-.bt-brand .bt-mark{flex:none} +-.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} +-.bt-rail .bt-row{position:relative} +-.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} +-.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +-[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} +-.bt-foot-account{order:-1;flex:none;display:inline-flex} +-[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} +- +-/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a +- loop while the install runs. */ +-.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-round.bt-update svg{width:17px;height:17px} +-.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} +-@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} +-.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} +-@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} +-.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-.bt-update-title{font-size:14px;line-height:20px;font-weight:600} +-.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} +-.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} +-.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} +-.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} +-.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} +-.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} +-.bt-update-go:hover:not(:disabled){opacity:.9} +-.bt-update-go:active:not(:disabled){transform:scale(.96)} +-.bt-update-go:disabled{opacity:.6;cursor:default} +-.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} +-@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} +- +-/* The DSH Agent row and its expanding Session list. The black whale is the +- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ +-.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} +-@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} +-.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-agent-sub[data-open]{grid-template-rows:1fr} +-.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} +-.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} +-.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} +-.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} +-.bt-agent-new{color:var(--bt-ink-3)} +-.bt-agent-new svg{width:14px;height:14px;flex:none} +-.bt-agent-edit{padding:3px 10px 3px 4px} +-.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} +-.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} +-@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} +-@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} +-`; ++ }; + if (state === null) return null; + return (0, import_react20.createElement)( + "div", @@ -15748,121 +15209,122 @@ diff --git a/client.js b/client.js + ) + ) + ); - } --async function transcribeWave(speech, catalog, wave, signal) { -- if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); -- const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); -- if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); -- return String(result.value?.text ?? "").trim(); -+ -+// src/client/header.js ++} + + // src/client/header.js +-var import_react9 = require("react"); +-var import_react_dom4 = require("react-dom"); +-var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); +var import_react21 = require("react"); +var import_react_dom6 = require("react-dom"); +var GoArrow = () => (0, import_react21.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react21.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); -+var MEMORY_NOTICE_MS = 3200; -+var noNews = (select) => select({}); -+function useMemoryNotice(ids, useMemoryNews) { -+ const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); + var MEMORY_NOTICE_MS = 3200; + var noNews = (select) => select({}); + function useMemoryNotice(ids, useMemoryNews) { + const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); +- const [shown, setShown] = (0, import_react9.useState)(false); +- (0, import_react9.useEffect)(() => { + const [shown, setShown] = (0, import_react21.useState)(false); + (0, import_react21.useEffect)(() => { -+ const left = seen + MEMORY_NOTICE_MS - Date.now(); -+ setShown(seen > 0 && left > 0); -+ if (seen === 0 || left <= 0) return void 0; -+ const timer = setTimeout(() => setShown(false), left); -+ return () => clearTimeout(timer); -+ }, [seen]); -+ return shown; + const left = seen + MEMORY_NOTICE_MS - Date.now(); + setShown(seen > 0 && left > 0); + if (seen === 0 || left <= 0) return void 0; +@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { + return shown; } --function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { -- let spokeAt = 0; -- let quietSince = 0; -- return (level, now) => { -- if (level >= loud) { -- spokeAt ||= now; -- quietSince = 0; -- return "speech"; -- } -- if (!spokeAt) return "waiting"; -- if (level < quiet) quietSince ||= now; -- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; -- }; -+function MemoryNotice({ shown }) { + function MemoryNotice({ shown }) { +- const inner = (0, import_react9.useRef)(null); +- const [width, setWidth] = (0, import_react9.useState)(0); + const inner = (0, import_react21.useRef)(null); + const [width, setWidth] = (0, import_react21.useState)(0); -+ const label = t("Memory updated"); + const label = t("Memory updated"); +- (0, import_react9.useLayoutEffect)(() => { + (0, import_react21.useLayoutEffect)(() => { -+ if (shown) setWidth(inner.current?.scrollWidth ?? 0); -+ }, [label, shown]); + if (shown) setWidth(inner.current?.scrollWidth ?? 0); + }, [label, shown]); +- return (0, import_react9.createElement)( + return (0, import_react21.createElement)( -+ "span", -+ { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, + "span", + { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, +- (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) + (0, import_react21.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react21.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react21.createElement)(MemoryIcon)), label) -+ ); + ); } -- --// src/client/overlay-hooks.js --var import_react11 = require("react"); --var escapeLayers = []; --var onEscapeKey = (event) => { -- if (event.key === "Escape") escapeLayers.at(-1)?.current(); --}; --function useEscape(onClose) { -- const ref = (0, import_react11.useRef)(onClose); -- ref.current = onClose; -- (0, import_react11.useEffect)(() => { -- if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); -- escapeLayers.push(ref); +-function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { +function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useBusyAcks, useMemoryNews, useTurnClock, useUi, useLive, actions }) { -+ const roster = useRoster((value) => value); -+ const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); -+ const firstTurns = useSessions((list) => { -+ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -+ return Array.isArray(outline) ? outline.slice(0, 8).map((entry) => entry.turn).join(",") : ""; -+ }); -+ const hiddenTurns = roster.exchangeTurns?.[sessionId]; -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const opening = useTurnClock((state) => { -+ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); -+ return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -+ }); + const roster = useRoster((value) => value); + const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); + const firstTurns = useSessions((list) => { +@@ -5537,7 +5537,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe + const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); + return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; + }); +- (0, import_react9.useEffect)(() => { + (0, import_react21.useEffect)(() => { -+ if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); -+ else document.body.style.removeProperty("--bt-first-sep"); -+ return () => document.body.style.removeProperty("--bt-first-sep"); -+ }, [opening]); -+ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -+ const owner = roster.byId[sessionId]?.id ?? sessionId; -+ const doing = useActivity((map) => map[owner]); -+ const bot = roster.byId[sessionId]; + if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); + else document.body.style.removeProperty("--bt-first-sep"); + return () => document.body.style.removeProperty("--bt-first-sep"); +@@ -5546,54 +5546,49 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe + const owner = roster.byId[sessionId]?.id ?? sessionId; + const doing = useActivity((map) => map[owner]); + const bot = roster.byId[sessionId]; + const projectAgent = projectAgentFor(bot, roster.self?.memberId); -+ const room2 = roster.roomsById[sessionId]; -+ const detailsOpen = useUi((state) => state.details === sessionId); + const room2 = roster.roomsById[sessionId]; + const detailsOpen = useUi((state) => state.details === sessionId); +- const name = bot?.name ?? room2?.name ?? ""; + const full2 = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; + const face = room2?.origin === "face-to-face"; + const name = face ? faceRowTitle(full2) : full2; -+ const useNews = useMemoryNews ?? noNews; -+ const memoryIds = bot ? [bot.id] : room2?.members ?? []; -+ const notice = useMemoryNotice(memoryIds, useNews); + const useNews = useMemoryNews ?? noNews; + const memoryIds = bot ? [bot.id] : room2?.members ?? []; + const notice = useMemoryNotice(memoryIds, useNews); +- const changed = useNews((map) => latestMemoryChange(map, memoryIds)); +- (0, import_react9.useEffect)(() => { +- document.body.style.setProperty("--bt-placeholder", JSON.stringify(name ? t("Message {name}", { name }) : t("Message"))); +- }, [name, t("Message")]); +- (0, import_react9.useEffect)(() => { +- actions.beginVisit(sessionId); +- return () => actions.endVisit(sessionId); +- }, [sessionId]); +- (0, import_react9.useEffect)(() => { +- if (room2) document.body.dataset.btRoom = ""; +- else delete document.body.dataset.btRoom; +- return () => { +- delete document.body.dataset.btRoom; +- }; +- }, [Boolean(room2)]); +- const memoryNotice = notice && changed !== null && roster.memory !== false; +- return (0, import_react9.createElement)( + const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); + const memoryNotice = notice && changed2 !== null && roster.memory !== false; + return (0, import_react21.createElement)( -+ "div", -+ { className: "bt-head" }, + "div", + { className: "bt-head" }, +- (0, import_react9.createElement)( + (0, import_react21.createElement)( -+ "button", -+ { -+ type: "button", -+ className: "bt-pill", -+ "aria-expanded": detailsOpen, -+ "data-memory": memoryNotice ? "updated" : void 0, + "button", + { + type: "button", + className: "bt-pill", + "aria-expanded": detailsOpen, + "data-memory": memoryNotice ? "updated" : void 0, +- "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed]?.name ?? name }) : t("View conversation details"), +- onClick: () => memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId) + "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed2]?.name ?? full2 }) : t("View conversation details"), + onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId) -+ }, + }, +- bot ? (0, import_react9.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, +- (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), +- (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), +- (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) + bot ? (0, import_react21.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react21.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react21.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), + (0, import_react21.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react21.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react21.createElement)(GoArrow)) -+ ), + ), +- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), +- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), +- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) + (0, import_react21.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + projectAgent === null ? null : (0, import_react21.createElement)(ProjectAgentEntry, { + projectId: projectAgent.projectId, @@ -15872,364 +15334,100 @@ diff --git a/client.js b/client.js + (0, import_react21.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), + (0, import_react21.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), + (0, import_react21.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) -+ ); -+} -+function useTranscriptView() { + ); + } + function useTranscriptView() { +- const [view, setView] = (0, import_react9.useState)(null); +- (0, import_react9.useEffect)(() => { + const [view, setView] = (0, import_react21.useState)(null); + (0, import_react21.useEffect)(() => { -+ let frame = 0; -+ let watched = null; -+ const observer = new MutationObserver(() => schedule()); -+ const measure = () => { -+ frame = 0; -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ if (scroller !== watched) { -+ observer.disconnect(); -+ watched = scroller; -+ if (scroller) observer.observe(scroller, { childList: true, subtree: true }); -+ } -+ if (!scroller) { -+ setView(null); -+ return; -+ } -+ const box = scroller.getBoundingClientRect(); -+ const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; -+ const sep = scroller.querySelector(".bt-new-sep"); -+ let divider = null; -+ if (sep) { -+ const at = sep.getBoundingClientRect(); -+ divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; -+ } -+ const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; -+ const next = { -+ unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, -+ divider, -+ center: Math.round(box.left + box.width / 2), -+ top: Math.round(headerBottom + 8), -+ bottom: Math.round(window.innerHeight - box.bottom + composer + 8) -+ }; -+ setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); -+ }; -+ const schedule = () => { -+ if (!frame) frame = requestAnimationFrame(measure); -+ }; -+ document.addEventListener("scroll", schedule, true); -+ window.addEventListener("resize", schedule); -+ schedule(); - return () => { -- escapeLayers.splice(escapeLayers.indexOf(ref), 1); -- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ document.removeEventListener("scroll", schedule, true); -+ window.removeEventListener("resize", schedule); - }; + let frame = 0; + let watched = null; + const observer = new MutationObserver(() => schedule()); +@@ -5642,7 +5642,7 @@ function useTranscriptView() { }, []); -+ return view; + return view; } --function usePaneLeft() { -- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); -- (0, import_react11.useLayoutEffect)(() => { -- const column = document.querySelector('[class*="_footArea"]')?.parentElement; -- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); -- }, []); -- return paneLeft; --} -- --// src/client/palette.js --var import_react12 = require("react"); --var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); --var PALETTE_EXIT_MS = 160; --function CommandPalette({ start, token, roster, actions, useSessions }) { -- const [query, setQuery] = (0, import_react12.useState)(start ?? ""); -- const [cursor, setCursor] = (0, import_react12.useState)(0); -- const [leaving, setLeaving] = (0, import_react12.useState)(false); -- const wanted = query.trim().toLowerCase(); -- const close = () => { -- if (leaving) return; -- setLeaving(true); -- setTimeout(() => actions.closePalette(token), PALETTE_EXIT_MS); -- }; -- useEscape(close); -- const ids = [...roster.bots.map(currentPart), ...roster.rooms.map((item) => item.id)]; -- const found = useSessions((list) => { -- if (wanted.length < 2) return "[]"; -- const hits = []; -- for (const id of ids) { -- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; -- if (!Array.isArray(outline)) continue; -- const hidden = roster.exchangeTurns[id] ?? []; -- for (let index2 = outline.length - 1; index2 >= 0 && hits.length < 30; index2 -= 1) { -- const entry = outline[index2]; -- if (hidden.includes(entry.turn)) continue; -- for (const raw of [entry.response, entry.prompt]) { -- const text = stripHeader(raw ?? "").replace(/\s+/g, " ").trim(); -- const at = text.toLowerCase().indexOf(wanted); -- if (at < 0) continue; -- const from = Math.max(0, at - 24); -- hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); -- break; -- } -- } +-function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }) { -+ const replies = useSessions((list) => { -+ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -+ return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -+ }); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const unread = useTurnClock((state) => { -+ if (typeof visit?.seen !== "number") return 0; -+ let count = 0; -+ for (const entry of Object.values(state[sessionId] ?? {})) { -+ if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; - } -- return JSON.stringify(hits); -+ return count; + const replies = useSessions((list) => { + const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; + return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; +@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { }); -- const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); -- const sections = []; -- const people = [ -- ...roster.bots.filter((bot) => matchesName(bot.name, bot.role)).map((bot) => ({ -- key: bot.id, -- avatar: (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), -- title: bot.name, -- subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), -- badge: bot.hidden ? t("Hidden") : void 0, -- run: async () => { -- if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); -- actions.openSession(bot.id); -- } -- })), -- ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ -- key: room2.id, -- avatar: (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -- title: room2.name, -- subtitle: t("{count} members", { count: room2.members.length }), -- badge: room2.hidden ? t("Hidden") : void 0, -- run: async () => { -- if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); -- actions.openSession(room2.id); -+ const view = useTranscriptView(); -+ const unpinned = view?.unpinned === true; + const view = useTranscriptView(); + const unpinned = view?.unpinned === true; +- const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); +- const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false }); +- (0, import_react9.useEffect)(() => { + const [release, setRelease] = (0, import_react21.useState)({ base: null, dismissed: false }); + const [above, setAbove] = (0, import_react21.useState)({ seen: false, dismissed: false }); + (0, import_react21.useEffect)(() => { -+ setRelease({ base: unpinned ? replies : null, dismissed: false }); -+ }, [unpinned]); + setRelease({ base: unpinned ? replies : null, dismissed: false }); + }, [unpinned]); +- (0, import_react9.useEffect)(() => { + (0, import_react21.useEffect)(() => { -+ if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); -+ }, [view?.divider]); + if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); + }, [view?.divider]); + const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); + (0, import_react21.useEffect)(() => { + if (above.seen && movedOn) actions?.settleVisit?.(sessionId); + }, [above.seen, movedOn, sessionId]); -+ const fresh = release.base === null ? 0 : replies - release.base; -+ const showDown = unpinned && fresh > 0 && !release.dismissed; -+ const showUp = view?.divider === "above" && !above.seen && !above.dismissed; + const fresh = release.base === null ? 0 : replies - release.base; + const showDown = unpinned && fresh > 0 && !release.dismissed; + const showUp = view?.divider === "above" && !above.seen && !above.dismissed; +- (0, import_react9.useEffect)(() => { + (0, import_react21.useEffect)(() => { -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ if (!scroller) return void 0; -+ scroller.toggleAttribute("data-bt-news", showDown); -+ return () => scroller.removeAttribute("data-bt-news"); -+ }, [showDown]); -+ if (!view || !showDown && !showUp) return null; -+ const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); + const scroller = document.querySelector("[data-conversation-scroll]"); + if (!scroller) return void 0; + scroller.toggleAttribute("data-bt-news", showDown); +@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + }, [showDown]); + if (!view || !showDown && !showUp) return null; + const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); +- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( + const pill = (direction, count, style, onJump, onDismiss) => (0, import_react21.createElement)( -+ "div", -+ { className: "bt-news", "data-direction": direction, style }, + "div", + { className: "bt-news", "data-direction": direction, style }, +- (0, import_react9.createElement)( + (0, import_react21.createElement)( -+ "button", -+ { type: "button", className: "bt-news-jump", onClick: onJump }, + "button", + { type: "button", className: "bt-news-jump", onClick: onJump }, +- (0, import_react9.createElement)("span", { className: "bt-news-ico" }, (0, import_react9.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), + (0, import_react21.createElement)("span", { className: "bt-news-ico" }, (0, import_react21.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -+ label(count) -+ ), + label(count) + ), +- (0, import_react9.createElement)("button", { + (0, import_react21.createElement)("button", { -+ type: "button", -+ className: "bt-news-x", -+ "aria-label": t("Dismiss new messages"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onDismiss(); + type: "button", + className: "bt-news-x", + "aria-label": t("Dismiss new messages"), +@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + event.stopPropagation(); + onDismiss(); } -- })) -- ]; -- if (people.length) sections.push([t("Bots"), people]); -- const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ -- key: agent.id, -- avatar: (0, import_react12.createElement)("span", { className: "bt-palette-agent" }, (0, import_react12.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), -- title: "DSH Agent", -- subtitle: t("Plain dsh session"), -- run: () => actions.openAgent(agent.id) -- })); -- if (agents.length) sections.push(["DSH Agent", agents]); -- const messages = JSON.parse(found).map(([id, turn, snippet]) => { -- const bot = roster.byId[id]; -- const room2 = roster.roomsById[id]; -- return { -- key: `${id}:${turn}`, -- title: bot?.name ?? room2?.name ?? t("Bot"), -- subtitle: snippet, -- avatar: bot ? (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -- run: () => actions.openSession(id) -- }; -- }); -- if (messages.length) sections.push([t("Messages"), messages]); -- const commands = [ -- { key: "cmd:new", icon: (0, import_react12.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -- { key: "cmd:bot", icon: (0, import_react12.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -- { key: "cmd:group", icon: (0, import_react12.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -- { key: "cmd:settings", icon: (0, import_react12.createElement)(GearIcon), title: t("Bot settings"), run: () => actions.openSettings("general") } -- ].filter((command) => matchesName(command.title)); -- if (commands.length) sections.push([t("Commands"), commands]); -- const rows = sections.flatMap(([, items]) => items); -- const active = Math.min(cursor, Math.max(rows.length - 1, 0)); -- const shown = rows.map((row) => row.key).join("\n"); -- const glide = useGlide(`${active} --${shown}`, { axis: "y", ...QUICK_EDGES }); -- const listRef = glide.box; -- useFlip(listRef, shown, "[data-flip]"); -- (0, import_react12.useEffect)(() => { -- listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); -- }, [active, query]); -- const choose = (row) => { -- if (!row || leaving) return; -- void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); -- close(); +- }, (0, import_react9.createElement)(CloseIcon)) + }, (0, import_react21.createElement)(CloseIcon)) -+ ); -+ const toLatest = () => { -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); -+ if (back) back.click(); -+ else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); + ); + const toLatest = () => { + const scroller = document.querySelector("[data-conversation-scroll]"); +@@ -5703,9 +5703,9 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); }; -- let index = -1; -- return (0, import_react12.createElement)( -- "div", -- { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -- (0, import_react12.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -- (0, import_react12.createElement)( -- "div", -- { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -- (0, import_react12.createElement)( -- "div", -- { className: "bt-palette-head" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), -- (0, import_react12.createElement)("input", { -- autoFocus: true, -- value: query, -- placeholder: t("Search Bots, chats and messages"), -- "aria-label": t("Search"), -- role: "combobox", -- "aria-expanded": true, -- "aria-controls": "bt-palette-list", -- onChange: (event) => { -- setQuery(event.target.value); -- setCursor(0); -- }, -- onKeyDown: (event) => { -- if (event.nativeEvent.isComposing) return; -- if (event.key === "ArrowDown") { -- event.preventDefault(); -- setCursor(Math.min(active + 1, rows.length - 1)); -- } else if (event.key === "ArrowUp") { -- event.preventDefault(); -- setCursor(Math.max(active - 1, 0)); -- } else if (event.key === "Enter") { -- event.preventDefault(); -- choose(rows[active]); -- } -- } -- }), -- query ? (0, import_react12.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { -- setQuery(""); -- setCursor(0); -- } }, (0, import_react12.createElement)(CloseIcon)) : null -- ), -- (0, import_react12.createElement)( -- "div", -- { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, -- (0, import_react12.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- rows.length === 0 ? (0, import_react12.createElement)( -- "div", -- { className: "bt-palette-empty" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -- ) : sections.map(([title, items]) => (0, import_react12.createElement)( -- import_react12.Fragment, -- { key: title }, -- (0, import_react12.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), -- items.map((row) => { -- index += 1; -- const at = index; -- return (0, import_react12.createElement)( -- "button", -- { -- key: row.key, -- type: "button", -- role: "option", -- className: "bt-palette-row", -- "aria-selected": at === active, -- "data-flip": row.key, -- onMouseMove: () => { -- if (cursor !== at) setCursor(at); -- }, -- onClick: () => choose(row) -- }, -- (0, import_react12.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react12.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -- (0, import_react12.createElement)( -- "span", -- { className: "bt-palette-text" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-title" }, row.title), -- row.subtitle ? (0, import_react12.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null -- ), -- row.badge ? (0, import_react12.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null -- ); -- }) -- )) -- ) -- ) -+ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); + 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, + return (0, import_react_dom6.createPortal)( + (0, import_react21.createElement)( + import_react21.Fragment, -+ null, -+ showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, -+ showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -+ ), -+ document.body + null, + showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, + showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null +@@ -5714,19 +5714,135 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { ); } --// src/client/model-menu.js --var import_react13 = require("react"); --var import_react_dom6 = require("react-dom"); --var MENU_EXIT_MS = 120; --function groupModels(models) { -- const groups = /* @__PURE__ */ new Map(); -- for (const model of models ?? []) { -- const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0]; -- if (!groups.has(provider)) groups.set(provider, []); -- groups.get(provider).push(model); -- } -- const names = /* @__PURE__ */ new Map(); -- for (const [provider, list] of groups) { -- const name = list[0].providerName ?? provider; -- names.set(name, (names.get(name) ?? 0) + 1); -- } -- return [...groups].map(([provider, list]) => { -- const name = list[0].providerName ?? provider; -- return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; -- }); --} --var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); --function modelOf(bot, roster) { -- const ref = bot.model ?? bot.appliedModel; -- if (!ref) return void 0; -- const served = servedOf(roster, ref); -- const [provider, ...rest] = ref.split("/"); -- return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; +// src/client/composer-label.js +var ATTRIBUTES = ["aria-label", "data-placeholder"]; +function holdComposerLabel(doc, label) { @@ -16260,20 +15458,7 @@ diff --git a/client.js b/client.js + } + } + }; - } --var effortCache = /* @__PURE__ */ new Map(); --function useEfforts(served, ref, actions) { -- const [fetched, setFetched] = (0, import_react13.useState)(void 0); -- (0, import_react13.useEffect)(() => { -- setFetched(void 0); -- if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; -- let live = true; -- if (effortCache.has(ref)) setFetched(effortCache.get(ref)); -- else void Promise.resolve(actions.efforts(ref)).then((value) => { -- const reasoning = value?.efforts ? value : null; -- effortCache.set(ref, reasoning); -- if (live) setFetched(reasoning); -- }).catch(() => { ++} + +// src/client/header-effects.js +function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { @@ -16303,12 +15488,7 @@ diff --git a/client.js b/client.js + return () => { + delete doc.body.dataset.btFriendChat; + }; - }); -- return () => { -- live = false; -- }; -- }, [ref, served?.reasoning !== void 0, actions]); -- return served?.reasoning ?? fetched; ++ }); + run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { + if (!bot?.remote || row?.blank !== true) return void 0; + doc.body.dataset.btRemoteChat = "empty"; @@ -16362,77 +15542,50 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; - } --function effortRows(reasoning) { -- if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; -- return [ -- { effort: void 0, label: t("Default") }, -- ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) -- ]; ++} + -+// src/client/composer.js + // src/client/composer.js +-var import_react10 = require("react"); +-var import_react_dom5 = require("react-dom"); +var import_react22 = require("react"); +var import_react_dom7 = require("react-dom"); -+var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; -+var keepComposerFocus = (event) => event.preventDefault(); -+function useDictation(onText) { + var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; + var keepComposerFocus = (event) => event.preventDefault(); + function useDictation(onText) { +- const [listening, setListening] = (0, import_react10.useState)(false); +- const [note, setNote] = (0, import_react10.useState)(""); +- const recognition = (0, import_react10.useRef)(null); +- const latest = (0, import_react10.useRef)(onText); + const [listening, setListening] = (0, import_react22.useState)(false); + const [note, setNote] = (0, import_react22.useState)(""); + const recognition = (0, import_react22.useRef)(null); + const latest = (0, import_react22.useRef)(onText); -+ latest.current = onText; + latest.current = onText; +- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); +- (0, import_react10.useEffect)(() => { + (0, import_react22.useEffect)(() => () => recognition.current?.abort(), []); + (0, import_react22.useEffect)(() => { -+ if (!note) return void 0; -+ const timer = setTimeout(() => setNote(""), 2600); -+ return () => clearTimeout(timer); -+ }, [note]); -+ const start = ({ continuous = true, onEnd } = {}) => { -+ const Api = speechApi(); -+ if (!Api) { -+ setNote(t("Dictation is not available in this browser")); -+ return false; -+ } -+ const engine = new Api(); -+ engine.lang = navigator.language || "zh-CN"; -+ engine.interimResults = true; -+ engine.continuous = continuous; -+ engine.onresult = (event) => { -+ let interim = ""; -+ for (let index = event.resultIndex; index < event.results.length; index += 1) { -+ const result = event.results[index]; + if (!note) return void 0; + const timer = setTimeout(() => setNote(""), 2600); + return () => clearTimeout(timer); +@@ -5745,10 +5745,10 @@ function useDictation(onText) { + let interim = ""; + for (let index = event.resultIndex; index < event.results.length; index += 1) { + const result = event.results[index]; +- const text = result[0]?.transcript ?? ""; + const text2 = result[0]?.transcript ?? ""; -+ if (result.isFinal) { + if (result.isFinal) { +- if (text.trim()) latest.current(text, true); +- } else interim += text; + if (text2.trim()) latest.current(text2, true); + } else interim += text2; -+ } -+ if (interim) latest.current(interim, false); -+ }; -+ engine.onerror = (event) => { -+ if (event.error === "aborted" || event.error === "no-speech") return; -+ setNote(event.error === "not-allowed" || event.error === "service-not-allowed" ? t("Microphone access is blocked") : t("Couldn't hear that")); -+ }; -+ engine.onend = () => { -+ setListening(false); -+ recognition.current = null; -+ onEnd?.(); -+ }; -+ recognition.current = engine; -+ try { -+ engine.start(); -+ setListening(true); -+ return true; -+ } catch { -+ setNote(t("Couldn't start dictation")); -+ return false; -+ } -+ }; -+ const stop = () => recognition.current?.stop(); -+ return { listening, note, start, stop }; + } + if (interim) latest.current(interim, false); + }; +@@ -5774,19 +5774,38 @@ function useDictation(onText) { + const stop = () => recognition.current?.stop(); + return { listening, note, start, stop }; } --function EffortRows({ reasoning, current, onPick }) { -- return (0, import_react13.createElement)( -- import_react13.Fragment, +function ComposerIdentity({ sessionId, useRoster }) { + const roster = useRoster((value) => value); + const room2 = roster.roomsById?.[sessionId]; @@ -16452,246 +15605,134 @@ diff --git a/client.js b/client.js + t("Sending as {name}", { name: self.displayName || self.memberId }) + ); +} -+function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +- const dictation = useDictation((text, final) => { +- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); + const dictation = useDictation((text2, final) => { + if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); -+ }); -+ const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); + }); + const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); +- (0, import_react10.useEffect)(() => { + (0, import_react22.useEffect)(() => { -+ if (!quote || !inputActions) return; -+ actions.clearQuote(quote.token); -+ const excerpt = quote.text.replace(/\s+/g, " ").trim(); + if (!quote || !inputActions) return; + actions.clearQuote(quote.token); + const excerpt = quote.text.replace(/\s+/g, " ").trim(); +- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} + const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} -+ -+`; + + `; +- inputActions.insertText(line, inputActions.captureInsertion()); + inputActions.insertText(line2, inputActions.captureInsertion()); -+ const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); -+ if (box instanceof HTMLElement) { -+ box.focus(); -+ const selection = window.getSelection(); -+ selection?.selectAllChildren(box); -+ selection?.collapseToEnd(); -+ } -+ }, [quote?.token]); -+ if (sessionId === void 0) return null; -+ const pluginMic = actions.speech() !== void 0; + const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); + if (box instanceof HTMLElement) { + box.focus(); +@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + }, [quote?.token]); + if (sessionId === void 0) return null; + const pluginMic = actions.speech() !== void 0; +- return (0, import_react10.createElement)( +- import_react10.Fragment, + return (0, import_react22.createElement)( + import_react22.Fragment, null, -- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -- effortRows(reasoning).map((row) => (0, import_react13.createElement)( -- "button", -- { -- key: row.effort ?? "default", -- type: "button", -- role: "menuitemradio", -- "aria-checked": row.effort === current, -- onClick: () => onPick(row.effort) -- }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) -- )) +- pluginMic ? null : (0, import_react10.createElement)("button", { + pluginMic ? null : (0, import_react22.createElement)("button", { -+ type: "button", -+ className: "bt-mic", -+ "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -+ "aria-pressed": dictation.listening, -+ title: dictation.listening ? t("Stop dictation") : t("Dictate"), -+ onMouseDown: keepComposerFocus, -+ onClick: () => dictation.listening ? dictation.stop() : dictation.start() + type: "button", + className: "bt-mic", + "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), +@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + title: dictation.listening ? t("Stop dictation") : t("Dictate"), + onMouseDown: keepComposerFocus, + onClick: () => dictation.listening ? dictation.stop() : dictation.start() +- }, (0, import_react10.createElement)(MicIcon)), +- (0, import_react10.createElement)("button", { + }, (0, import_react22.createElement)(MicIcon)), + (0, import_react22.createElement)("button", { -+ type: "button", -+ className: "bt-voice", -+ "aria-label": t("Start voice chat"), -+ title: t("Voice chat"), -+ onMouseDown: keepComposerFocus, -+ onClick: () => actions.openVoice(sessionId) + type: "button", + className: "bt-voice", + "aria-label": t("Start voice chat"), + title: t("Voice chat"), + onMouseDown: keepComposerFocus, + onClick: () => actions.openVoice(sessionId) +- }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), +- dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null + }, (0, import_react22.createElement)(WaveIcon, { size: 18 })), + dictation.note ? (0, import_react22.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ); } --function ModelRows({ roster, current, onPick }) { -- return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)( -- import_react13.Fragment, -- { key: list[0].provider ?? provider }, -- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -- list.map((model) => (0, import_react13.createElement)( -- "button", -- { -- key: model.ref, -- type: "button", -- role: "menuitemradio", -- "aria-checked": model.ref === current, -- title: model.ref, -- onClick: () => onPick(model.ref) -- }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), -- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) -- )) -- )); -+function menuIcons(roster) { -+ const marks = /* @__PURE__ */ new Map(); -+ return (row) => { -+ if (!row.bot) return PeopleIcon; -+ const id = row.bot.id; + function menuIcons(roster) { +@@ -5825,17 +5825,17 @@ function menuIcons(roster) { + return (row) => { + if (!row.bot) return PeopleIcon; + const id = row.bot.id; +- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); + if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react22.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); -+ return marks.get(id); -+ }; + return marks.get(id); + }; } --var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); --function useAnchoredMenu() { -- const [place, setPlace] = (0, import_react13.useState)(null); -- const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); -- const button = (0, import_react13.useRef)(null); -- const menu = (0, import_react13.useRef)(null); -- const close = () => setPlace(null); -- (0, import_react13.useEffect)(() => { -- if (place === null) return void 0; -+function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { -+ const roster = useRoster((value) => value); + function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + const roster = useRoster((value) => value); +- const [open, setOpen] = (0, import_react10.useState)(null); + const [open, setOpen] = (0, import_react22.useState)(null); -+ const [shown, leaving] = useLinger(open, 120); + const [shown, leaving] = useLinger(open, 120); +- const button = (0, import_react10.useRef)(null); +- const menu = (0, import_react10.useRef)(null); +- (0, import_react10.useEffect)(() => { + const button = (0, import_react22.useRef)(null); + const menu = (0, import_react22.useRef)(null); + (0, import_react22.useEffect)(() => { -+ if (!open) return void 0; + if (!open) return void 0; const onDown = (event) => { -- if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -- close(); -+ if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); - }; - const onKey = (event) => { - if (event.key === "Escape") { - event.stopPropagation(); -- close(); -- button.current?.focus(); -+ setOpen(null); - } - }; -- const onScroll = (event) => { -- if (!menu.current?.contains(event.target)) close(); -- }; -- window.addEventListener("mousedown", onDown); -+ window.addEventListener("mousedown", onDown, true); - window.addEventListener("keydown", onKey, true); -- window.addEventListener("scroll", onScroll, true); - return () => { -- window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("mousedown", onDown, true); - window.removeEventListener("keydown", onKey, true); -- window.removeEventListener("scroll", onScroll, true); - }; -- }, [place]); -- const open = () => { -- const rect = button.current.getBoundingClientRect(); -- const width = Math.max(240, rect.width); -- const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8)); -- const below = window.innerHeight - rect.bottom; -- setPlace(below >= 260 || below >= rect.top ? { left, width, top: rect.bottom + 4, maxHeight: below - 16, "--bt-origin": "top right" } : { left, width, bottom: window.innerHeight - rect.top + 4, maxHeight: rect.top - 16, "--bt-origin": "bottom right" }); -- }; -- return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; --} --function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { -- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); -- const shown = modelOf(bot, roster); -- const current = bot.model ?? bot.appliedModel; -- const served = servedOf(roster, current); -- const reasoning = useEfforts(served, current, actions); -- const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0; -- const currentEffort = picked ?? reasoning?.defaultEffort; -- const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort; -+ }, [open]); -+ const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; -+ if (sessionId === void 0 || target === void 0) return null; + if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); +@@ -5855,6 +5855,7 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + }, [open]); + const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; + if (sessionId === void 0 || target === void 0) return null; + const locked = lockedWindow(roster); const toggle = () => { -- if (isOpen) { -- close(); -+ if (open) { -+ setOpen(null); - return; - } -- void actions?.refreshModels?.(); -- open(); -+ const rect = button.current.getBoundingClientRect(); -+ setOpen({ left: rect.left, bottom: window.innerHeight - rect.top + 8 }); - }; -- const pick2 = (ref) => { -- close(); -- if (onPick) onPick(ref); -- else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); -+ const attach = () => { -+ setOpen(null); -+ button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); + if (open) { + setOpen(null); +@@ -5867,19 +5867,15 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + setOpen(null); + button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); }; -- const pickEffort = (effort) => { -- close(); -- if (onPickEffort) onPickEffort(effort); -- else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); -+ const reference = () => { -+ setOpen(null); -+ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); +- const schedule = () => { +- setOpen(null); +- actions.openSettings("schedules", target); +- }; + const reference = () => { + setOpen(null); + actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; -- const models = roster.models ?? []; -- return (0, import_react13.createElement)( -- import_react13.Fragment, -+ const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); + const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); +- return (0, import_react10.createElement)( +- import_react10.Fragment, + return (0, import_react22.createElement)( + import_react22.Fragment, null, -- (0, import_react13.createElement)( -- "button", +- (0, import_react10.createElement)("button", { + (0, import_react22.createElement)("button", { -+ ref: button, -+ type: "button", -+ className: "bt-plus", -+ "aria-label": t("Add"), -+ title: t("Add"), -+ "aria-haspopup": "menu", -+ "aria-expanded": Boolean(open), -+ onMouseDown: keepComposerFocus, -+ onClick: toggle + ref: button, + type: "button", + className: "bt-plus", +@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + "aria-expanded": Boolean(open), + onMouseDown: keepComposerFocus, + onClick: toggle +- }, (0, import_react10.createElement)(PlusIcon)), +- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( + }, (0, import_react22.createElement)(PlusIcon)), + shown ? (0, import_react_dom7.createPortal)((0, import_react22.createElement)( -+ "div", + "div", { -- ref: button, -- type: "button", -- className: "bt-model-trigger", -- "data-wide": wide || void 0, -- "aria-haspopup": "menu", -- "aria-expanded": isOpen, -- "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), -- "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, -- onClick: toggle -+ ref: menu, -+ className: "bt-menu bt-menu-up bt-plus-menu", -+ role: "menu", -+ "aria-label": t("Add"), -+ style: { left: shown.left, bottom: shown.bottom }, -+ "data-leaving": leaving || void 0 + ref: menu, +@@ -5900,9 +5900,12 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + style: { left: shown.left, bottom: shown.bottom }, + "data-leaving": leaving || void 0 }, -- (0, import_react13.createElement)( -- "span", -- { className: "bt-model-copy" }, -- (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -- shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null -- ), -- (0, import_react13.createElement)(Caret) -- ), -- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( -- "div", -- { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, -- models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -- effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -- close(); -- openHarnessSettings(); -- } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), +- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) + (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react22.createElement)(PaperclipIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), + canReference ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react22.createElement)(ChatsIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, + locked ? null : (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => { @@ -16701,95 +15742,11 @@ diff --git a/client.js b/client.js ), document.body) : null ); } --function ModelChoiceDock({ sessionId, useRoster, actions }) { -- const roster = useRoster((value) => value); -- const bot = roster.byId[sessionId]; -- const [busy, setBusy] = (0, import_react13.useState)(""); -- const [error, setError] = (0, import_react13.useState)(""); -- const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; -- (0, import_react13.useEffect)(() => { -- if (shown) void actions.refreshModels?.(); -- }, [shown, sessionId]); -- if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; -- const models = roster.models ?? []; -- const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; -- if (bot.model && (!lost || models.length === 0)) return null; -- const pick2 = (ref) => { -- setBusy(ref); -- setError(""); -- void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { -- setError(failure?.message ?? String(failure)); -- void actions.refreshModels?.(); -- }).finally(() => setBusy("")); -- }; -- const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); -- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ -- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( -- "div", -- { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, -- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { -- key: model.ref, -- type: "button", -- className: "bt-model-chip", -- disabled: busy !== "", -- "aria-busy": busy === model.ref || void 0, -- "aria-label": `${model.name} · ${provider}`, -- title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, -- onClick: () => pick2(model.ref) -- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -- )), -- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) -- ]; -- return (0, import_react13.createElement)( -- "div", -- { className: "bt-model-dock" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-astack", "data-lead": "" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-msg" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-msg-line" }, -- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) -- ) -- ) -- ), -- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -- ); --} --function setupChoices(actions) { -- return [ -- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, -- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], -- [t("Open settings to set up a custom model"), openHarnessSettings] -- ].filter(Boolean); --} --function DeepSeekSignIn({ useUi, actions }) { -- const open = useUi((state) => state.signIn); -- const entry = open ? actions.signInEntry?.() : null; -- if (!entry) return null; -- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); -+ -+// src/client/at-menu.js -+var BOTS_SOURCE = "ds-bot"; -+var CHATS_SOURCE = "ds-bot-chats"; -+var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; -+var OTHER_CHATS_MAX = 8; -+var conversationOf = (roster, sessionId) => { -+ if (!roster.ready) return null; -+ const room2 = roster.roomsById[sessionId]; -+ if (room2) return { room: room2 }; -+ const self = roster.byId[sessionId]; -+ return self ? { self } : null; -+}; -+function mentionRows(roster, sessionId, query = "") { -+ const here = conversationOf(roster, sessionId); -+ if (here === null) return null; +@@ -5922,15 +5922,57 @@ var conversationOf = (roster, sessionId) => { + function mentionRows(roster, sessionId, query = "") { + const here = conversationOf(roster, sessionId); + if (here === null) return null; +- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); + const face = isFaceRoom(here.room); + const live = (here.room?.participants ?? []).filter((p) => p.leftSeq === void 0); + const bots = here.room ? [ @@ -16807,12 +15764,15 @@ diff --git a/client.js b/client.js + label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) + })) + ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); -+ const needle = query.trim().toLowerCase(); + const needle = query.trim().toLowerCase(); +- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); + const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); -+ const everyone = t("Everyone"); -+ if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { + const everyone = t("Everyone"); + if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { +- rows.push({ room: here.room, text: `@${everyone} ` }); + rows.push({ room: here.room, count: bots.filter((bot) => bot.kind !== "human").length, text: `@${everyone} ` }); -+ } + } +- return rows; + if (face) return rows; + const seatHandles = new Set(bots.filter((bot) => bot.remote).map((bot) => normHandle(remoteRecord(bot)?.handle)).filter(Boolean)); + if (here.room) return [...rows, ...remoteMentionRows(roster, query).filter((row) => !seatHandles.has(normHandle(row.remote.handle)))]; @@ -16822,20 +15782,6 @@ diff --git a/client.js b/client.js + ...remoteMentionRows(roster, query) + ]; } --function SignInHost({ entry, actions }) { -- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); -- const close = () => actions.closeSignIn(); -- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -- return (0, import_react13.createElement)(entry.component, { -- ...ops, -- ...hooks, -- t: actions.accountT(), -- complete: close, -- useApiKey: () => { -- close(); -- openHarnessOnboarding("deepseek-official"); -- } +var isFaceRoom = (room2) => room2?.origin === "face-to-face"; +var memberCount = (count) => t(count === 1 ? "{count} member" : "{count} members", { count }); +var botCount = (count) => count === 1 ? t("1 Bot") : t("{count} Bots", { count }); @@ -16855,34 +15801,17 @@ diff --git a/client.js b/client.js + }); +}; +var faceLabel = (person, bot) => person ? `${bot} (${person})` : bot; -+function chatRows(roster, list, sessionId) { -+ const here = conversationOf(roster, sessionId); -+ if (here === null) return []; -+ const byId = list?.byId ?? {}; -+ const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { -+ const id = currentPart(bot); -+ return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; - }); -+ const team = /* @__PURE__ */ new Set([ -+ ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), -+ ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) -+ ]); -+ const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); -+ return [...bots, ...others]; -+} -+function sessionMention(sessionId, label) { -+ let binary = ""; -+ for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); -+ const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -+ return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; -+} -+function atSources({ roster, sessions, launched, iconFor }) { -+ const bots = { -+ trigger: "@", -+ name: BOTS_SOURCE, -+ order: -20, -+ showGroupTitle: false, -+ async candidates(session, { query }) { + function chatRows(roster, list, sessionId) { + const here = conversationOf(roster, sessionId); + if (here === null) return []; +@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { + order: -20, + showGroupTitle: false, + async candidates(session, { query }) { +- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ +- name: row.bot ? row.bot.name : t("Everyone"), +- ...row.bot?.role ? { description: row.bot.role } : {}, +- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, + const snapshot = roster.getSnapshot(); + const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); + const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ @@ -16895,10 +15824,10 @@ diff --git a/client.js b/client.js + ...row.bot?.remote && !face && remoteRecord(row.bot).handle ? { description: remoteRecord(row.bot).handle } : {}, + // A face room counts the Bots in it, mine and everyone else's (#305, #457). + ...row.room ? { description: face ? botCount(row.count) : memberCount(row.count) } : {}, -+ icon: iconFor(row), -+ section: t("Bots"), -+ value: row.text -+ })); + icon: iconFor(row), + section: t("Bots"), + value: row.text + })); + if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; + const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ + name: row.name, @@ -16908,92 +15837,13 @@ diff --git a/client.js b/client.js + value: row.value + })); + return [...team, ...extra]; -+ }, -+ onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 -+ }; -+ const chats = { -+ trigger: "@", -+ name: CHATS_SOURCE, -+ order: -10, -+ showGroupTitle: false, -+ async candidates(session) { -+ if (!launched(session.sessionId)) return []; -+ return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ -+ name: row.bot ? row.bot.name : row.label, -+ ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, -+ icon: row.bot ? iconFor(row) : "session", -+ section: row.bot ? t("Bots") : t("Conversations"), -+ value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) -+ })); -+ }, -+ onPick({ candidate }) { -+ const { sessionId, label } = JSON.parse(candidate.value); -+ const mention = sessionMention(sessionId, label); -+ return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; -+ }, -+ codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } -+ }; -+ return [bots, chats]; + }, + onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 + }; +@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { + return [bots, chats]; } --var MODEL_CSS = ` --.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} --.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} --.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} --.bt-model-trigger[data-missing] .bt-model-provider{color:#c21d2e} --.bt-model-trigger[data-wide]{display:flex;width:100%;min-height:48px;padding:7px 12px;justify-content:space-between;border-radius:12px;background:var(--bt-card);box-shadow:inset 0 0 0 1px var(--bt-line)} --.bt-model-trigger[data-wide]:hover,.bt-model-trigger[data-wide][aria-expanded=true]{background:var(--bt-hover)} --.bt-model-copy{display:flex;flex-direction:column;min-width:0} --.bt-model-name{font-size:13px;line-height:18px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-model-provider{font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-model-caret{flex:none;color:var(--bt-ink-3);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-model-trigger[aria-expanded=true] .bt-model-caret{transform:rotate(180deg)} --.bt-model-menu{overflow-y:auto;scrollbar-width:thin} --.bt-model-group{flex:none;padding:8px 8px 2px;font-size:11px;line-height:14px;font-weight:500;color:var(--bt-ink-3);text-transform:none} --.bt-model-group:first-child{padding-top:2px} --.bt-model-tick{flex:none;width:16px;height:16px;display:inline-flex;color:var(--bt-ink-2)} --.bt-model-tick svg{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 60ms backwards} --.bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;margin-left:30px;max-width:min(560px,calc(100% - 30px));max-height:min(240px,36vh);overflow-y:auto;scrollbar-width:thin} --.bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0} --.bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere} --.bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px} --.bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease} --.bt-model-chip:hover:not(:disabled){background:var(--bt-hover)} --.bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} --.bt-model-chip-quiet{color:var(--bt-ink-2)} --.bt-model-chip svg{width:14px;height:14px} --.bt-model-chip:disabled{cursor:default} --.bt-model-chip:disabled:not([aria-busy]){opacity:.5} --.bt-model-error{margin-left:30px} --@keyframes bt-model-in{from{opacity:0;transform:translateY(6px)}} --.bt-model-spin{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--bt-line-2);border-top-color:var(--bt-ink-2);animation:bt-model-spin .7s linear infinite} --@keyframes bt-model-spin{to{transform:rotate(360deg)}} --@media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} --`; --// src/client/folder-menu.js --var import_react14 = require("react"); --var import_react_dom7 = require("react-dom"); --var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); --var Tick = ({ on }) => (0, import_react14.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react14.createElement)(CheckIcon) : null); --function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { -- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); -- const [info, setInfo] = (0, import_react14.useState)(null); -- const [view, setView] = (0, import_react14.useState)("list"); -- const [draft, setDraft] = (0, import_react14.useState)(""); -- const [listing, setListing] = (0, import_react14.useState)(null); -- const [problem, setProblem] = (0, import_react14.useState)(""); -- const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); -- const toggle = () => { -- if (isOpen) { -- close(); -- return; -- } -- setView("list"); -- setProblem(""); -- void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); -- open(); +// src/client/face-at-filter.js +var REFERENCE_SOURCE = "reference"; +var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; @@ -17041,10 +15891,7 @@ diff --git a/client.js b/client.js +} +function watchFaceMenu(controller, doc = globalThis.document) { + if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { - }; -- const choose = (path) => { -- close(); -- if (path !== (value ?? "")) void onPick(path); ++ }; + const style = doc.createElement("style"); + style.setAttribute("data-bt-face-at", ""); + doc.head.appendChild(style); @@ -17064,715 +15911,772 @@ diff --git a/client.js b/client.js + if (!stopped) controller.hover(plan.target.source, plan.target.index); + }); + else previous = state.open ? state.highlight : null; - }; -- const browse = (path) => { -- setProblem(""); -- void Promise.resolve(actions.folderList(path)).then((next) => { -- setListing(next); -- setView("browse"); -- }).catch((failure) => setProblem(failure?.message ?? String(failure))); ++ }; + const unsubscribe = controller.menu.subscribe(apply2); + apply2(); + return () => { + stopped = true; + unsubscribe?.(); + style.remove(); - }; -- const pickNative = () => { -- close(); -- void run(Promise.resolve(actions.folderPick()).then((path) => path ? onPick(path) : void 0)); ++ }; +} + -+// src/client/speech.js -+var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; -+var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); -+async function speechCatalog(speech) { -+ const result = await speech.catalog(); -+ if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); -+ const catalog = result.value; -+ const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); -+ return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; -+} -+function waveBytes(samples, rate = 16e3) { -+ const bytes = new Uint8Array(44 + samples.length * 2); -+ const view = new DataView(bytes.buffer); + // src/client/speech.js + var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; + var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); +@@ -6008,8 +6008,8 @@ async function speechCatalog(speech) { + function waveBytes(samples, rate = 16e3) { + const bytes = new Uint8Array(44 + samples.length * 2); + const view = new DataView(bytes.buffer); +- const ascii = (at, text) => { +- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); + const ascii = (at, text2) => { + for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); }; -- const typed = () => { -- const path = draft.trim(); -- if (path !== "") choose(path); -+ ascii(0, "RIFF"); -+ view.setUint32(4, 36 + samples.length * 2, true); -+ ascii(8, "WAVEfmt "); -+ view.setUint32(16, 16, true); -+ view.setUint16(20, 1, true); -+ view.setUint16(22, 1, true); -+ view.setUint32(24, rate, true); -+ view.setUint32(28, rate * 2, true); -+ view.setUint16(32, 2, true); -+ view.setUint16(34, 16, true); -+ ascii(36, "data"); -+ view.setUint32(40, samples.length * 2, true); -+ samples.forEach((sample, index) => { -+ const value = Math.max(-1, Math.min(1, sample)); -+ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); -+ }); -+ return bytes; -+} -+function base64Of(bytes) { + ascii(0, "RIFF"); + view.setUint32(4, 36 + samples.length * 2, true); +@@ -6030,9 +6030,9 @@ function waveBytes(samples, rate = 16e3) { + return bytes; + } + function base64Of(bytes) { +- let text = ""; +- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); +- return btoa(text); + let text2 = ""; + for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); + return btoa(text2); -+} -+var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); -+async function openCapture() { -+ const devices = navigator.mediaDevices; -+ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); -+ let stream; -+ try { -+ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); -+ } catch (error) { -+ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); -+ } -+ const audio = new AudioContext(); -+ const analyser = audio.createAnalyser(); -+ analyser.fftSize = 512; -+ audio.createMediaStreamSource(stream).connect(analyser); -+ const frame = new Float32Array(analyser.fftSize); -+ const recorder = new MediaRecorder(stream); -+ const chunks = []; -+ recorder.ondataavailable = (event) => { -+ if (event.data.size > 0) chunks.push(event.data); - }; -- const known = info?.folders ?? []; -- const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; -- const listView = (0, import_react14.createElement)( -- "div", -- { key: "list", className: "bt-folder-view" }, -- (0, import_react14.createElement)( -- "button", -- { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, -- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, fallback), -- (0, import_react14.createElement)(Tick, { on: !value }) -- ), -- folders.length > 0 ? (0, import_react14.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -- folders.map((folder) => (0, import_react14.createElement)( -- "button", -- { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, -- (0, import_react14.createElement)("span", { className: "bt-folder-copy" }, (0, import_react14.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react14.createElement)("span", { className: "bt-folder-path" }, folder.path)), -- (0, import_react14.createElement)(Tick, { on: folder.path === value }) -- )), -- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- info?.picker === "native" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -- info?.picker === "browse" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -- (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -- setDraft(value ?? ""); -- setView("type"); -- } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) -- ); -- const typeView = (0, import_react14.createElement)( -- "div", -- { key: "type", className: "bt-folder-view" }, -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react14.createElement)("input", { -- className: "bt-folder-input", -- autoFocus: true, -- value: draft, -- spellCheck: false, -- "aria-label": t("Folder path"), -- placeholder: t("Full path of a folder"), -- onChange: (event) => setDraft(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); -+ recorder.start(); -+ let closed = false; -+ const close = () => { -+ if (closed) return; -+ closed = true; -+ if (recorder.state !== "inactive") recorder.stop(); -+ stream.getTracks().forEach((track) => track.stop()); -+ void audio.close().catch(() => { -+ }); -+ }; -+ return { -+ level() { -+ if (closed) return 0; -+ analyser.getFloatTimeDomainData(frame); -+ let sum = 0; -+ for (const sample of frame) sum += sample * sample; -+ return Math.min(1, Math.sqrt(sum / frame.length) * 4); -+ }, -+ cancel: close, -+ async finish(maxSeconds) { -+ if (closed || recorder.state === "inactive") { -+ close(); -+ return null; - } -- }), -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) -- ); -- const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; -- const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; -- const browseView = listing ? (0, import_react14.createElement)( -- "div", -- { key: `browse:${listing.path}`, className: "bt-folder-view" }, -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -- parent ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -- inside.map((entry) => (0, import_react14.createElement)( -- "button", -- { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, -- (0, import_react14.createElement)(FolderIcon), -- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, entry.name), -- (0, import_react14.createElement)(ChevronRightIcon) -- )), -- inside.length === 0 ? (0, import_react14.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -- listing.truncated ? (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) -- ) : null; -- const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; -- return (0, import_react14.createElement)( -- import_react14.Fragment, -- null, -- (0, import_react14.createElement)( -- "button", -- { -- ref: button, -- type: "button", -- className: "bt-model-trigger", -- "data-wide": wide || void 0, -- "aria-haspopup": "menu", -- "aria-expanded": isOpen, -- "aria-label": t("Working folder: {folder}", { folder: value ?? fallback }), -- title: value, -- onClick: toggle -- }, -- (0, import_react14.createElement)( -- "span", -- { className: "bt-model-copy" }, -- (0, import_react14.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -- value ? (0, import_react14.createElement)("span", { className: "bt-model-provider" }, value) : null -- ), -- (0, import_react14.createElement)(Caret) -- ), -- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( -- "div", -- { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, -- shownView, -- problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null -- ), document.body) : null -- ); -+ try { -+ await new Promise((resolve) => { -+ recorder.onstop = resolve; -+ recorder.stop(); -+ }); -+ stream.getTracks().forEach((track) => track.stop()); -+ const blob = new Blob(chunks, { type: recorder.mimeType }); -+ if (blob.size === 0) return null; -+ const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); -+ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); -+ const offline = new OfflineAudioContext(1, length, 16e3); -+ const source = offline.createBufferSource(); -+ source.buffer = decoded; -+ source.connect(offline.destination); -+ source.start(); -+ return waveBytes((await offline.startRendering()).getChannelData(0)); -+ } finally { -+ close(); -+ } -+ } -+ }; -+} -+async function transcribeWave(speech, catalog, wave, signal) { -+ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); -+ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); -+ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); -+ return String(result.value?.text ?? "").trim(); -+} -+function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { -+ let spokeAt = 0; -+ let quietSince = 0; -+ return (level, now) => { -+ if (level >= loud) { -+ spokeAt ||= now; -+ quietSince = 0; -+ return "speech"; -+ } -+ if (!spokeAt) return "waiting"; -+ if (level < quiet) quietSince ||= now; -+ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; -+ }; } --var FOLDER_CSS = ` --.bt-folder-view{display:flex;flex-direction:column;gap:2px;animation:bt-veil-in .14s ease-out} --.bt-folder-copy{flex:1;display:flex;flex-direction:column;min-width:0} --.bt-folder-copy .bt-menu-label{flex:none} --.bt-folder-path{min-width:0;font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-folder-here{padding:0 8px 4px;white-space:normal;overflow-wrap:anywhere} --.bt-menu .bt-folder-back{color:var(--bt-ink-2);font-weight:500} --.bt-folder-input{height:30px;margin:2px 4px;padding:0 8px;border:1px solid var(--bt-line-2);border-radius:8px;background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;outline:none;box-sizing:border-box} --.bt-folder-input:focus{border-color:var(--bt-accent)} --.bt-menu .bt-folder-use{justify-content:center;font-weight:500;color:var(--bt-accent)} --.bt-menu .bt-folder-use:disabled{color:var(--bt-ink-3);cursor:default;background:none} --.bt-folder-error{padding:4px 8px;font-size:12px;line-height:16px;color:#c21d2e;overflow-wrap:anywhere} --@media (prefers-reduced-motion:reduce){.bt-folder-view{animation:none}} --`; - --// src/client/schedules.js --var import_react15 = require("react"); --var POLL_MS = 3e4; --var shared = null; --var listeners = /* @__PURE__ */ new Set(); --function publish(view) { -- shared = view; -- for (const listener of listeners) listener(view); -+// src/client/model-menu.js -+var import_react23 = require("react"); -+var import_react_dom8 = require("react-dom"); -+var MENU_EXIT_MS = 120; -+function groupModels(models) { -+ const groups = /* @__PURE__ */ new Map(); -+ for (const model of models ?? []) { -+ const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0]; -+ if (!groups.has(provider)) groups.set(provider, []); -+ groups.get(provider).push(model); -+ } -+ const names = /* @__PURE__ */ new Map(); -+ for (const [provider, list] of groups) { -+ const name = list[0].providerName ?? provider; -+ names.set(name, (names.get(name) ?? 0) + 1); -+ } -+ return [...groups].map(([provider, list]) => { -+ const name = list[0].providerName ?? provider; -+ return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; -+ }); + var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); + async function openCapture() { +@@ -6116,238 +6116,14 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { + return "speech"; + } + if (!spokeAt) return "waiting"; +- if (level < quiet) quietSince ||= now; +- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; +- }; +-} +- +-// src/client/overlay-hooks.js +-var import_react11 = require("react"); +-var escapeLayers = []; +-var onEscapeKey = (event) => { +- if (event.key === "Escape") escapeLayers.at(-1)?.current(); +-}; +-function useEscape(onClose) { +- const ref = (0, import_react11.useRef)(onClose); +- ref.current = onClose; +- (0, import_react11.useEffect)(() => { +- if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); +- escapeLayers.push(ref); +- return () => { +- escapeLayers.splice(escapeLayers.indexOf(ref), 1); +- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); +- }; +- }, []); +-} +-function usePaneLeft() { +- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); +- (0, import_react11.useLayoutEffect)(() => { +- const column = document.querySelector('[class*="_footArea"]')?.parentElement; +- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); +- }, []); +- return paneLeft; +-} +- +-// src/client/palette.js +-var import_react12 = require("react"); +-var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); +-var PALETTE_EXIT_MS = 160; +-function CommandPalette({ start, token, roster, actions, useSessions }) { +- const [query, setQuery] = (0, import_react12.useState)(start ?? ""); +- const [cursor, setCursor] = (0, import_react12.useState)(0); +- const [leaving, setLeaving] = (0, import_react12.useState)(false); +- const wanted = query.trim().toLowerCase(); +- const close = () => { +- if (leaving) return; +- setLeaving(true); +- setTimeout(() => actions.closePalette(token), PALETTE_EXIT_MS); +- }; +- useEscape(close); +- const ids = [...roster.bots.map(currentPart), ...roster.rooms.map((item) => item.id)]; +- const found = useSessions((list) => { +- if (wanted.length < 2) return "[]"; +- const hits = []; +- for (const id of ids) { +- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; +- if (!Array.isArray(outline)) continue; +- const hidden = roster.exchangeTurns[id] ?? []; +- for (let index2 = outline.length - 1; index2 >= 0 && hits.length < 30; index2 -= 1) { +- const entry = outline[index2]; +- if (hidden.includes(entry.turn)) continue; +- for (const raw of [entry.response, entry.prompt]) { +- const text = stripHeader(raw ?? "").replace(/\s+/g, " ").trim(); +- const at = text.toLowerCase().indexOf(wanted); +- if (at < 0) continue; +- const from = Math.max(0, at - 24); +- hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); +- break; +- } +- } +- } +- return JSON.stringify(hits); +- }); +- const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); +- const sections = []; +- const people = [ +- ...roster.bots.filter((bot) => matchesName(bot.name, bot.role)).map((bot) => ({ +- key: bot.id, +- avatar: (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), +- title: bot.name, +- subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), +- badge: bot.hidden ? t("Hidden") : void 0, +- run: async () => { +- if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); +- actions.openSession(bot.id); +- } +- })), +- ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ +- key: room2.id, +- avatar: (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), +- title: room2.name, +- subtitle: t("{count} members", { count: room2.members.length }), +- badge: room2.hidden ? t("Hidden") : void 0, +- run: async () => { +- if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); +- actions.openSession(room2.id); +- } +- })) +- ]; +- if (people.length) sections.push([t("Bots"), people]); +- const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ +- key: agent.id, +- avatar: (0, import_react12.createElement)("span", { className: "bt-palette-agent" }, (0, import_react12.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), +- title: "DSH Agent", +- subtitle: t("Plain dsh session"), +- run: () => actions.openAgent(agent.id) +- })); +- if (agents.length) sections.push(["DSH Agent", agents]); +- const messages = JSON.parse(found).map(([id, turn, snippet]) => { +- const bot = roster.byId[id]; +- const room2 = roster.roomsById[id]; +- return { +- key: `${id}:${turn}`, +- title: bot?.name ?? room2?.name ?? t("Bot"), +- subtitle: snippet, +- avatar: bot ? (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), +- run: () => actions.openSession(id) +- }; +- }); +- if (messages.length) sections.push([t("Messages"), messages]); +- const commands = [ +- { key: "cmd:new", icon: (0, import_react12.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, +- { key: "cmd:bot", icon: (0, import_react12.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, +- { key: "cmd:group", icon: (0, import_react12.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, +- { key: "cmd:settings", icon: (0, import_react12.createElement)(GearIcon), title: t("Bot settings"), run: () => actions.openSettings("general") } +- ].filter((command) => matchesName(command.title)); +- if (commands.length) sections.push([t("Commands"), commands]); +- const rows = sections.flatMap(([, items]) => items); +- const active = Math.min(cursor, Math.max(rows.length - 1, 0)); +- const shown = rows.map((row) => row.key).join("\n"); +- const glide = useGlide(`${active} +-${shown}`, { axis: "y", ...QUICK_EDGES }); +- const listRef = glide.box; +- useFlip(listRef, shown, "[data-flip]"); +- (0, import_react12.useEffect)(() => { +- listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); +- }, [active, query]); +- const choose = (row) => { +- if (!row || leaving) return; +- void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); +- close(); ++ if (level < quiet) quietSince ||= now; ++ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; + }; +- let index = -1; +- return (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, +- (0, import_react12.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), +- (0, import_react12.createElement)( +- "div", +- { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, +- (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-head" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), +- (0, import_react12.createElement)("input", { +- autoFocus: true, +- value: query, +- placeholder: t("Search Bots, chats and messages"), +- "aria-label": t("Search"), +- role: "combobox", +- "aria-expanded": true, +- "aria-controls": "bt-palette-list", +- onChange: (event) => { +- setQuery(event.target.value); +- setCursor(0); +- }, +- onKeyDown: (event) => { +- if (event.nativeEvent.isComposing) return; +- if (event.key === "ArrowDown") { +- event.preventDefault(); +- setCursor(Math.min(active + 1, rows.length - 1)); +- } else if (event.key === "ArrowUp") { +- event.preventDefault(); +- setCursor(Math.max(active - 1, 0)); +- } else if (event.key === "Enter") { +- event.preventDefault(); +- choose(rows[active]); +- } +- } +- }), +- query ? (0, import_react12.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { +- setQuery(""); +- setCursor(0); +- } }, (0, import_react12.createElement)(CloseIcon)) : null +- ), +- (0, import_react12.createElement)( +- "div", +- { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, +- (0, import_react12.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- rows.length === 0 ? (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-empty" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) +- ) : sections.map(([title, items]) => (0, import_react12.createElement)( +- import_react12.Fragment, +- { key: title }, +- (0, import_react12.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), +- items.map((row) => { +- index += 1; +- const at = index; +- return (0, import_react12.createElement)( +- "button", +- { +- key: row.key, +- type: "button", +- role: "option", +- className: "bt-palette-row", +- "aria-selected": at === active, +- "data-flip": row.key, +- onMouseMove: () => { +- if (cursor !== at) setCursor(at); +- }, +- onClick: () => choose(row) +- }, +- (0, import_react12.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react12.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), +- (0, import_react12.createElement)( +- "span", +- { className: "bt-palette-text" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-title" }, row.title), +- row.subtitle ? (0, import_react12.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null +- ), +- row.badge ? (0, import_react12.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null +- ); +- }) +- )) +- ) +- ) +- ); } --function useSchedules(actions) { -- const [view, setView] = (0, import_react15.useState)(shared); -- (0, import_react15.useEffect)(() => { -- listeners.add(setView); -- let stopped = false; -- let timer; -- const load = () => actions.schedules?.().then((next) => { -- if (!stopped) publish(next); -+var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); -+var effortCache = /* @__PURE__ */ new Map(); -+function useEfforts(served, ref, actions) { + + // src/client/model-menu.js +-var import_react13 = require("react"); +-var import_react_dom6 = require("react-dom"); ++var import_react23 = require("react"); ++var import_react_dom8 = require("react-dom"); + var MENU_EXIT_MS = 120; + function groupModels(models) { + const groups = /* @__PURE__ */ new Map(); +@@ -6367,17 +6367,10 @@ function groupModels(models) { + }); + } + var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); +-function modelOf(bot, roster) { +- const ref = bot.model ?? bot.appliedModel; +- if (!ref) return void 0; +- const served = servedOf(roster, ref); +- const [provider, ...rest] = ref.split("/"); +- return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; +-} + var effortCache = /* @__PURE__ */ new Map(); + function useEfforts(served, ref, actions) { +- const [fetched, setFetched] = (0, import_react13.useState)(void 0); +- (0, import_react13.useEffect)(() => { + const [fetched, setFetched] = (0, import_react23.useState)(void 0); + (0, import_react23.useEffect)(() => { -+ setFetched(void 0); -+ if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; -+ let live = true; -+ if (effortCache.has(ref)) setFetched(effortCache.get(ref)); -+ else void Promise.resolve(actions.efforts(ref)).then((value) => { -+ const reasoning = value?.efforts ? value : null; -+ effortCache.set(ref, reasoning); -+ if (live) setFetched(reasoning); - }).catch(() => { -- }).finally(() => { -- if (!stopped) timer = setTimeout(load, POLL_MS); - }); -- load(); - return () => { -- stopped = true; -- clearTimeout(timer); -- listeners.delete(setView); -+ live = false; - }; -- }, []); -- return view; -+ }, [ref, served?.reasoning !== void 0, actions]); -+ return served?.reasoning ?? fetched; - } --var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone; --var clock2 = (time) => String(time ?? "").slice(0, 5); --var when2 = (ms, options) => new Intl.DateTimeFormat(dateLocale(), options).format(ms); --function dayAndTime(ms) { -- const sameYear = new Date(ms).getFullYear() === (/* @__PURE__ */ new Date()).getFullYear(); -- return when2(ms, { ...sameYear ? {} : { year: "numeric" }, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); -+function effortRows(reasoning) { -+ if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; -+ return [ -+ { effort: void 0, label: t("Default") }, -+ ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) -+ ]; + setFetched(void 0); + if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; + let live = true; +@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { + ]; } --function weekdayNames(days) { -- const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); -- return names.join(dateLocale().startsWith("zh") ? "、" : ", "); -+function EffortRows({ reasoning, current, onPick }) { + function EffortRows({ reasoning, current, onPick }) { +- return (0, import_react13.createElement)( +- import_react13.Fragment, + return (0, import_react23.createElement)( + import_react23.Fragment, -+ null, + null, +- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), +- effortRows(reasoning).map((row) => (0, import_react13.createElement)( + (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), + effortRows(reasoning).map((row) => (0, import_react23.createElement)( -+ "button", -+ { -+ key: row.effort ?? "default", -+ type: "button", -+ role: "menuitemradio", -+ "aria-checked": row.effort === current, -+ onClick: () => onPick(row.effort) -+ }, + "button", + { + key: row.effort ?? "default", +@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { + "aria-checked": row.effort === current, + onClick: () => onPick(row.effort) + }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) + (0, import_react23.createElement)("span", { className: "bt-menu-label" }, row.label), + (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react23.createElement)(CheckIcon) : null) -+ )) -+ ); + )) + ); } --function everyText(seconds) { -- if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); -- if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); -- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); -+function ModelRows({ roster, current, onPick }) { + function ModelRows({ roster, current, onPick }) { +- return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)( +- import_react13.Fragment, + return groupModels(roster.models).map(([provider, list]) => (0, import_react23.createElement)( + import_react23.Fragment, -+ { key: list[0].provider ?? provider }, + { key: list[0].provider ?? provider }, +- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), +- list.map((model) => (0, import_react13.createElement)( + (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), + list.map((model) => (0, import_react23.createElement)( -+ "button", -+ { -+ key: model.ref, -+ type: "button", -+ role: "menuitemradio", -+ "aria-checked": model.ref === current, -+ title: model.ref, -+ onClick: () => onPick(model.ref) -+ }, + "button", + { + key: model.ref, +@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { + title: model.ref, + onClick: () => onPick(model.ref) + }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), +- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) + (0, import_react23.createElement)("span", { className: "bt-menu-label" }, model.name), + model.ref === roster.defaultModel ? (0, import_react23.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, + (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react23.createElement)(CheckIcon) : null) -+ )) -+ )); + )) + )); } --function timingText(task) { -- const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -- switch (task.kind) { -- case "after": -- case "at": -- return t("Once, {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }); -- case "every": -- return everyText(task.everySeconds); -- case "daily": -- return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; -- case "weekly": { -- const days = (task.weekdays ?? []).join(); -- const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); -- return `${text}${zone}`; -- } -- case "cron": -- return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -- default: -- return ""; -- } +-var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); +var Caret = () => (0, import_react23.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react23.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); -+function useAnchoredMenu() { + function useAnchoredMenu() { +- const [place, setPlace] = (0, import_react13.useState)(null); + const [place, setPlace] = (0, import_react23.useState)(null); -+ const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); + const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); +- const button = (0, import_react13.useRef)(null); +- const menu = (0, import_react13.useRef)(null); + const button = (0, import_react23.useRef)(null); + const menu = (0, import_react23.useRef)(null); -+ const close = () => setPlace(null); + const close = () => setPlace(null); +- (0, import_react13.useEffect)(() => { + (0, import_react23.useEffect)(() => { -+ if (place === null) return void 0; -+ const onDown = (event) => { -+ if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -+ close(); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ close(); -+ button.current?.focus(); -+ } -+ }; -+ const onScroll = (event) => { -+ if (!menu.current?.contains(event.target)) close(); -+ }; -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); -+ window.addEventListener("scroll", onScroll, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); -+ window.removeEventListener("scroll", onScroll, true); -+ }; -+ }, [place]); -+ const open = () => { -+ const rect = button.current.getBoundingClientRect(); -+ const width = Math.max(240, rect.width); -+ const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8)); -+ const below = window.innerHeight - rect.bottom; -+ setPlace(below >= 260 || below >= rect.top ? { left, width, top: rect.bottom + 4, maxHeight: below - 16, "--bt-origin": "top right" } : { left, width, bottom: window.innerHeight - rect.top + 4, maxHeight: rect.top - 16, "--bt-origin": "bottom right" }); -+ }; -+ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; + if (place === null) return void 0; + const onDown = (event) => { + if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; +@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { + return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } --var nextText = (task) => task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); --function TaskRow({ task, actions, onView, showBot, roster }) { -- const [confirming, setConfirming] = (0, import_react15.useState)(false); -- const [busy, setBusy] = (0, import_react15.useState)(false); -- (0, import_react15.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 3e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const remove = () => { -- if (!confirming) { -- setConfirming(true); -+function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { + function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { + if (bot?.remote !== void 0) return null; -+ const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); -+ const shown = modelOf(bot, roster); -+ const current = bot.model ?? bot.appliedModel; -+ const served = servedOf(roster, current); -+ const reasoning = useEfforts(served, current, actions); -+ const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0; -+ const currentEffort = picked ?? reasoning?.defaultEffort; -+ const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort; -+ const toggle = () => { -+ if (isOpen) { -+ close(); - return; - } -- setBusy(true); -- void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); -+ void actions?.refreshModels?.(); -+ open(); + const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); + const shown = modelOf(bot, roster); + const current = bot.model ?? bot.appliedModel; +@@ -6511,10 +6511,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo + else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; -- const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; -- return (0, import_react15.createElement)( -- "div", -- { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, -- showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -- (0, import_react15.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), -- task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null -+ const pick2 = (ref) => { -+ close(); -+ if (onPick) onPick(ref); -+ else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); -+ }; -+ const pickEffort = (effort) => { -+ close(); -+ if (onPickEffort) onPickEffort(effort); -+ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); -+ }; -+ const models = roster.models ?? []; + const models = roster.models ?? []; +- return (0, import_react13.createElement)( +- import_react13.Fragment, + return (0, import_react23.createElement)( + import_react23.Fragment, -+ null, + null, +- (0, import_react13.createElement)( + (0, import_react23.createElement)( -+ "button", -+ { -+ ref: button, -+ type: "button", -+ className: "bt-model-trigger", -+ "data-wide": wide || void 0, -+ "aria-haspopup": "menu", -+ "aria-expanded": isOpen, -+ "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), -+ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, -+ onClick: toggle -+ }, + "button", + { + ref: button, +@@ -6527,37 +6527,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo + "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, + onClick: toggle + }, +- (0, import_react13.createElement)( + (0, import_react23.createElement)( -+ "span", -+ { className: "bt-model-copy" }, + "span", + { className: "bt-model-copy" }, +- (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), +- shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null + (0, import_react23.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), + shown ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null -+ ), + ), +- (0, import_react13.createElement)(Caret) + (0, import_react23.createElement)(Caret) ), -- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), -- (0, import_react15.createElement)("button", { -- type: "button", -- className: "bt-mini bt-task-remove", -- "data-confirm": confirming || void 0, -- disabled: busy, -- "aria-label": confirming ? t("Click again to delete") : t("Delete task"), -- title: confirming ? void 0 : t("Delete task"), -- onClick: remove -- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) +- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( + shownPlace ? (0, import_react_dom8.createPortal)((0, import_react23.createElement)( -+ "div", -+ { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, + "div", + { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, +- models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), +- effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, +- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => { + models.length > 0 ? (0, import_react23.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -+ effortRows(reasoning).length > 0 ? (0, import_react23.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -+ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -+ close(); -+ openHarnessModels(); -+ } }, (0, import_react23.createElement)(SlidersIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) -+ ), document.body) : null - ); - } --var pad = (number) => String(number).padStart(2, "0"); --function inAnHour() { -- const date = new Date(Date.now() + 36e5); -- date.setMinutes(0, 0, 0); -- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; --} --var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; --var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]]; --function TaskForm({ roster, actions, start, onDone, onCancel }) { -- const [target, setTarget] = (0, import_react15.useState)(start ?? roster.bots[0]?.id ?? ""); -- const [title, setTitle] = (0, import_react15.useState)(""); -- const [prompt, setPrompt] = (0, import_react15.useState)(""); -- const [kind, setKind] = (0, import_react15.useState)("daily"); -- const [at, setAt] = (0, import_react15.useState)(inAnHour); -- const [time, setTime] = (0, import_react15.useState)("09:00"); -- const [weekdays, setWeekdays] = (0, import_react15.useState)([1, 2, 3, 4, 5]); -- const [count, setCount] = (0, import_react15.useState)(1); -- const [unit, setUnit] = (0, import_react15.useState)("hours"); -- const [busy, setBusy] = (0, import_react15.useState)(false); -- const [error, setError] = (0, import_react15.useState)(""); -- const timing = () => { -- if (kind === "at") return { kind, at: new Date(at).toISOString() }; -- if (kind === "every") return { kind, minutes: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; -- return { kind, time, timeZone: localZone(), ...kind === "weekly" ? { weekdays } : {} }; -- }; -- const submit = (event) => { -- event.preventDefault(); -- if (busy) return; -- setBusy(true); -+function ModelChoiceDock({ sessionId, useRoster, actions }) { -+ const roster = useRoster((value) => value); -+ const bot = roster.byId[sessionId]; ++ effortRows(reasoning).length > 0 ? (0, import_react23.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, ++ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => { + close(); +- openHarnessSettings(); +- } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) ++ openHarnessModels(); ++ } }, (0, import_react23.createElement)(SlidersIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) + ), document.body) : null + ); + } + function ModelChoiceDock({ sessionId, useRoster, actions }) { + const roster = useRoster((value) => value); + const bot = roster.byId[sessionId]; +- const [busy, setBusy] = (0, import_react13.useState)(""); +- const [error, setError] = (0, import_react13.useState)(""); + const [busy, setBusy] = (0, import_react23.useState)(""); + const [error, setError] = (0, import_react23.useState)(""); -+ const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; + const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; +- (0, import_react13.useEffect)(() => { + (0, import_react23.useEffect)(() => { -+ if (shown) void actions.refreshModels?.(); -+ }, [shown, sessionId]); -+ if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; + if (shown) void actions.refreshModels?.(); + }, [shown, sessionId]); + if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; + if (bot.remote) return bot.remote.gone ? (0, import_react23.createElement)(RemoteGoneDock, { bot }) : null; -+ const models = roster.models ?? []; -+ const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; -+ if (bot.model && (!lost || models.length === 0)) return null; -+ const pick2 = (ref) => { -+ setBusy(ref); - setError(""); -- let when3; -- try { -- when3 = timing(); -- } catch { -- setError(t("Pick a date and time")); -- setBusy(false); -- return; -- } -- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { -- setError(hostText(failure?.message ?? String(failure))); -- setBusy(false); -- }); -+ void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { -+ setError(failure?.message ?? String(failure)); -+ void actions.refreshModels?.(); -+ }).finally(() => setBusy("")); + const models = roster.models ?? []; + const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; + if (bot.model && (!lost || models.length === 0)) return null; +@@ -6569,13 +6569,13 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { + void actions.refreshModels?.(); + }).finally(() => setBusy("")); }; -- const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); -- return (0, import_react15.createElement)( -- "form", -- { className: "bt-set-card bt-task-form", onSubmit: submit }, -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -- (0, import_react15.createElement)( -- "select", -- { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, -- (0, import_react15.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react15.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -- roster.rooms.length > 0 ? (0, import_react15.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react15.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null +- const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); +- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ +- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( + const text2 = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); + const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react23.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react23.createElement)(KeyHelpLink, { key: "key-help" })] : [ + ...groupModels(models).map(([provider, list]) => (0, import_react23.createElement)( -+ "div", -+ { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, + "div", + { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, +- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), +- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { + (0, import_react23.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), + (0, import_react23.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react23.createElement)("button", { -+ key: model.ref, -+ type: "button", -+ className: "bt-model-chip", -+ disabled: busy !== "", -+ "aria-busy": busy === model.ref || void 0, -+ "aria-label": `${model.name} · ${provider}`, -+ title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, -+ onClick: () => pick2(model.ref) + key: model.ref, + type: "button", + className: "bt-model-chip", +@@ -6584,586 +6584,411 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { + "aria-label": `${model.name} · ${provider}`, + title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, + onClick: () => pick2(model.ref) +- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) + }, model.name, busy === model.ref ? (0, import_react23.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -+ )), + )), +- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) + (0, import_react23.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react23.createElement)(SlidersIcon), t("Manage models")) -+ ]; + ]; +- return (0, import_react13.createElement)( + return (0, import_react23.createElement)( -+ "div", -+ { className: "bt-model-dock" }, + "div", + { className: "bt-model-dock" }, +- (0, import_react13.createElement)( + (0, import_react23.createElement)( -+ "div", -+ { className: "bt-astack", "data-lead": "" }, + "div", + { className: "bt-astack", "data-lead": "" }, +- (0, import_react13.createElement)( + (0, import_react23.createElement)( -+ "div", -+ { className: "bt-msg" }, + "div", + { className: "bt-msg" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-msg-line" }, +- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) +- ) +- ) +- ), +- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), +- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null +- ); +-} +-function setupChoices(actions) { +- return [ +- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, +- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], +- [t("Open settings to set up a custom model"), openHarnessSettings] +- ].filter(Boolean); +-} +-function DeepSeekSignIn({ useUi, actions }) { +- const open = useUi((state) => state.signIn); +- const entry = open ? actions.signInEntry?.() : null; +- if (!entry) return null; +- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); +-} +-function SignInHost({ entry, actions }) { +- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); +- const close = () => actions.closeSignIn(); +- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); +- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); +- return (0, import_react13.createElement)(entry.component, { +- ...ops, +- ...hooks, +- t: actions.accountT(), +- complete: close, +- useApiKey: () => { +- close(); +- openHarnessOnboarding("deepseek-official"); +- } +- }); +-} +-var MODEL_CSS = ` +-.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} +-.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} +-.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} +-.bt-model-trigger[data-missing] .bt-model-provider{color:#c21d2e} +-.bt-model-trigger[data-wide]{display:flex;width:100%;min-height:48px;padding:7px 12px;justify-content:space-between;border-radius:12px;background:var(--bt-card);box-shadow:inset 0 0 0 1px var(--bt-line)} +-.bt-model-trigger[data-wide]:hover,.bt-model-trigger[data-wide][aria-expanded=true]{background:var(--bt-hover)} +-.bt-model-copy{display:flex;flex-direction:column;min-width:0} +-.bt-model-name{font-size:13px;line-height:18px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-model-provider{font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-model-caret{flex:none;color:var(--bt-ink-3);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-model-trigger[aria-expanded=true] .bt-model-caret{transform:rotate(180deg)} +-.bt-model-menu{overflow-y:auto;scrollbar-width:thin} +-.bt-model-group{flex:none;padding:8px 8px 2px;font-size:11px;line-height:14px;font-weight:500;color:var(--bt-ink-3);text-transform:none} +-.bt-model-group:first-child{padding-top:2px} +-.bt-model-tick{flex:none;width:16px;height:16px;display:inline-flex;color:var(--bt-ink-2)} +-.bt-model-tick svg{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 60ms backwards} +-.bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;margin-left:30px;max-width:min(560px,calc(100% - 30px));max-height:min(240px,36vh);overflow-y:auto;scrollbar-width:thin} +-.bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0} +-.bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere} +-.bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px} +-.bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease} +-.bt-model-chip:hover:not(:disabled){background:var(--bt-hover)} +-.bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} +-.bt-model-chip-quiet{color:var(--bt-ink-2)} +-.bt-model-chip svg{width:14px;height:14px} +-.bt-model-chip:disabled{cursor:default} +-.bt-model-chip:disabled:not([aria-busy]){opacity:.5} +-.bt-model-error{margin-left:30px} +-@keyframes bt-model-in{from{opacity:0;transform:translateY(6px)}} +-.bt-model-spin{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--bt-line-2);border-top-color:var(--bt-ink-2);animation:bt-model-spin .7s linear infinite} +-@keyframes bt-model-spin{to{transform:rotate(360deg)}} +-@media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} +-`; +- +-// src/client/folder-menu.js +-var import_react14 = require("react"); +-var import_react_dom7 = require("react-dom"); +-var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); +-var Tick = ({ on }) => (0, import_react14.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react14.createElement)(CheckIcon) : null); +-function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { +- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); +- const [info, setInfo] = (0, import_react14.useState)(null); +- const [view, setView] = (0, import_react14.useState)("list"); +- const [draft, setDraft] = (0, import_react14.useState)(""); +- const [listing, setListing] = (0, import_react14.useState)(null); +- const [problem, setProblem] = (0, import_react14.useState)(""); +- const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); +- const toggle = () => { +- if (isOpen) { +- close(); +- return; +- } +- setView("list"); +- setProblem(""); +- void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); +- open(); +- }; +- const choose = (path) => { +- close(); +- if (path !== (value ?? "")) void onPick(path); +- }; +- const browse = (path) => { +- setProblem(""); +- void Promise.resolve(actions.folderList(path)).then((next) => { +- setListing(next); +- setView("browse"); +- }).catch((failure) => setProblem(failure?.message ?? String(failure))); +- }; +- const pickNative = () => { +- close(); +- void run(Promise.resolve(actions.folderPick()).then((path) => path ? onPick(path) : void 0)); +- }; +- const typed = () => { +- const path = draft.trim(); +- if (path !== "") choose(path); +- }; +- const known = info?.folders ?? []; +- const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; +- const listView = (0, import_react14.createElement)( +- "div", +- { key: "list", className: "bt-folder-view" }, +- (0, import_react14.createElement)( +- "button", +- { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, +- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, fallback), +- (0, import_react14.createElement)(Tick, { on: !value }) +- ), +- folders.length > 0 ? (0, import_react14.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, +- folders.map((folder) => (0, import_react14.createElement)( +- "button", +- { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, +- (0, import_react14.createElement)("span", { className: "bt-folder-copy" }, (0, import_react14.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react14.createElement)("span", { className: "bt-folder-path" }, folder.path)), +- (0, import_react14.createElement)(Tick, { on: folder.path === value }) +- )), +- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- info?.picker === "native" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, +- info?.picker === "browse" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, +- (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => { +- setDraft(value ?? ""); +- setView("type"); +- } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) +- ); +- const typeView = (0, import_react14.createElement)( +- "div", +- { key: "type", className: "bt-folder-view" }, +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), +- (0, import_react14.createElement)("input", { +- className: "bt-folder-input", +- autoFocus: true, +- value: draft, +- spellCheck: false, +- "aria-label": t("Folder path"), +- placeholder: t("Full path of a folder"), +- onChange: (event) => setDraft(event.target.value), +- onKeyDown: (event) => { +- if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); +- } +- }), +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) +- ); +- const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; +- const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; +- const browseView = listing ? (0, import_react14.createElement)( +- "div", +- { key: `browse:${listing.path}`, className: "bt-folder-view" }, +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), +- (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), +- parent ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, +- inside.map((entry) => (0, import_react14.createElement)( +- "button", +- { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, +- (0, import_react14.createElement)(FolderIcon), +- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, entry.name), +- (0, import_react14.createElement)(ChevronRightIcon) +- )), +- inside.length === 0 ? (0, import_react14.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, +- listing.truncated ? (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, +- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) +- ) : null; +- const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; +- return (0, import_react14.createElement)( +- import_react14.Fragment, +- null, +- (0, import_react14.createElement)( +- "button", +- { +- ref: button, +- type: "button", +- className: "bt-model-trigger", +- "data-wide": wide || void 0, +- "aria-haspopup": "menu", +- "aria-expanded": isOpen, +- "aria-label": t("Working folder: {folder}", { folder: value ?? fallback }), +- title: value, +- onClick: toggle +- }, +- (0, import_react14.createElement)( +- "span", +- { className: "bt-model-copy" }, +- (0, import_react14.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), +- value ? (0, import_react14.createElement)("span", { className: "bt-model-provider" }, value) : null +- ), +- (0, import_react14.createElement)(Caret) + (0, import_react23.createElement)( + "div", + { className: "bt-msg-line" }, + (0, import_react23.createElement)(BotMark, { bot, size: 22 }), + (0, import_react23.createElement)("div", { className: "bt-bubble" }, text2) + ) - ) ++ ) ), -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react15.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) -- ), -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -- (0, import_react15.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) -- ), -- (0, import_react15.createElement)( +- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( +- "div", +- { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, +- shownView, +- problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null +- ), document.body) : null + (0, import_react23.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), + error ? (0, import_react23.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -+ ); -+} + ); + } +-var FOLDER_CSS = ` +-.bt-folder-view{display:flex;flex-direction:column;gap:2px;animation:bt-veil-in .14s ease-out} +-.bt-folder-copy{flex:1;display:flex;flex-direction:column;min-width:0} +-.bt-folder-copy .bt-menu-label{flex:none} +-.bt-folder-path{min-width:0;font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-folder-here{padding:0 8px 4px;white-space:normal;overflow-wrap:anywhere} +-.bt-menu .bt-folder-back{color:var(--bt-ink-2);font-weight:500} +-.bt-folder-input{height:30px;margin:2px 4px;padding:0 8px;border:1px solid var(--bt-line-2);border-radius:8px;background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;outline:none;box-sizing:border-box} +-.bt-folder-input:focus{border-color:var(--bt-accent)} +-.bt-menu .bt-folder-use{justify-content:center;font-weight:500;color:var(--bt-accent)} +-.bt-menu .bt-folder-use:disabled{color:var(--bt-ink-3);cursor:default;background:none} +-.bt-folder-error{padding:4px 8px;font-size:12px;line-height:16px;color:#c21d2e;overflow-wrap:anywhere} +-@media (prefers-reduced-motion:reduce){.bt-folder-view{animation:none}} +-`; +- +-// src/client/schedules.js +-var import_react15 = require("react"); +-var POLL_MS = 3e4; +-var shared = null; +-var listeners = /* @__PURE__ */ new Set(); +-function publish(view) { +- shared = view; +- for (const listener of listeners) listener(view); +-} +-function useSchedules(actions) { +- const [view, setView] = (0, import_react15.useState)(shared); +- (0, import_react15.useEffect)(() => { +- listeners.add(setView); +- let stopped = false; +- let timer; +- const load = () => actions.schedules?.().then((next) => { +- if (!stopped) publish(next); +- }).catch(() => { +- }).finally(() => { +- if (!stopped) timer = setTimeout(load, POLL_MS); +- }); +- load(); +- return () => { +- stopped = true; +- clearTimeout(timer); +- listeners.delete(setView); +- }; +- }, []); +- return view; +function RemoteGoneDock({ bot }) { + return (0, import_react23.createElement)( + "div", + { className: "bt-model-dock", "data-flybot": "remote-gone" }, + (0, import_react23.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("When"))), -- (0, import_react15.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react15.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -- (0, import_react15.createElement)( ++ "div", + { className: "bt-astack", "data-lead": "" }, + (0, import_react23.createElement)( - "div", -- { key: kind, className: "bt-task-when bt-veil" }, -- kind === "at" ? (0, import_react15.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, -- kind === "weekly" ? (0, import_react15.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react15.createElement)("button", { -- key: day, -- type: "button", -- className: "bt-task-day", -- "aria-pressed": weekdays.includes(day), -- onClick: () => toggleDay(day) -- }, when2(Date.UTC(2024, 0, day), { weekday: "narrow", timeZone: "UTC" })))) : null, -- kind === "daily" || kind === "weekly" ? (0, import_react15.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, -- kind === "every" ? (0, import_react15.createElement)( -- import_react15.Fragment, -- null, -- (0, import_react15.createElement)("span", { className: "bt-task-every" }, t("Every")), -- (0, import_react15.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), -- (0, import_react15.createElement)( -- "select", -- { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, -- UNITS.map(([id, , label]) => (0, import_react15.createElement)("option", { key: id, value: id }, t(label))) -- ) -- ) : null ++ "div", + { className: "bt-msg" }, + (0, import_react23.createElement)( + "div", @@ -17780,17 +16684,26 @@ diff --git a/client.js b/client.js + (0, import_react23.createElement)(BotMark, { bot, size: 22 }), + (0, import_react23.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) + ) - ) ++ ) + ) + ); -+} + } +-var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone; +-var clock2 = (time) => String(time ?? "").slice(0, 5); +-var when2 = (ms, options) => new Intl.DateTimeFormat(dateLocale(), options).format(ms); +-function dayAndTime(ms) { +- const sameYear = new Date(ms).getFullYear() === (/* @__PURE__ */ new Date()).getFullYear(); +- return when2(ms, { ...sameYear ? {} : { year: "numeric" }, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); +function setupChoices(actions) { + return [ + actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, + [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], + [t("Open settings to set up a custom model"), openHarnessModels] + ].filter(Boolean); -+} + } +-function weekdayNames(days) { +- const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); +- return names.join(dateLocale().startsWith("zh") ? "、" : ", "); +var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; +function KeyHelpLink() { + return (0, import_react23.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); @@ -17827,27 +16740,39 @@ diff --git a/client.js b/client.js + (0, import_react23.createElement)("span", { className: "bt-turn-error-title" }, title), + (0, import_react23.createElement)("span", { className: "bt-turn-error-message" }, message), + code ? (0, import_react23.createElement)("code", { className: "bt-turn-error-code" }, code) : null - ), -- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -- (0, import_react15.createElement)( ++ ), + choices.length > 0 ? (0, import_react23.createElement)( - "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) -- ) ++ "div", + { className: "bt-turn-error-actions" }, + choices.map(([label, run]) => (0, import_react23.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), + deepseek ? (0, import_react23.createElement)(KeyHelpLink, { key: "key-help" }) : null + ) : null + ); -+} + } +-function everyText(seconds) { +- if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); +- if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); +- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); +function DeepSeekSignIn({ useUi, actions }) { + const open = useUi((state) => state.signIn); + const entry = open ? actions.signInEntry?.() : null; + if (!entry) return null; + return (0, import_react23.createElement)(SignInHost, { key: open, entry, actions }); -+} + } +-function timingText(task) { +- const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; +- switch (task.kind) { +- case "after": +- case "at": +- return t("Once, {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }); +- case "every": +- return everyText(task.everySeconds); +- case "daily": +- return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; +- case "weekly": { +- const days = (task.weekdays ?? []).join(); +- const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); +- return `${text}${zone}`; +function SignInHost({ entry, actions }) { + const [ops] = (0, import_react23.useState)(() => entry.inject?.() ?? {}); + const close = () => actions.closeSignIn(); @@ -17861,9 +16786,30 @@ diff --git a/client.js b/client.js + useApiKey: () => { + close(); + openHarnessOnboarding("deepseek-official"); -+ } + } +- case "cron": +- return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; +- default: +- return ""; +- } + }); -+} + } +-var nextText = (task) => task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); +-function TaskRow({ task, actions, onView, showBot, roster }) { +- const [confirming, setConfirming] = (0, import_react15.useState)(false); +- const [busy, setBusy] = (0, import_react15.useState)(false); +- (0, import_react15.useEffect)(() => { +- if (!confirming) return void 0; +- const timer = setTimeout(() => setConfirming(false), 3e3); +- return () => clearTimeout(timer); +- }, [confirming]); +- const remove = () => { +- if (!confirming) { +- setConfirming(true); +- return; +- } +- setBusy(true); +- void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); +var MODEL_CSS = ` +.bt-turn-error{display:grid;grid-template-columns:10px minmax(0,1fr);column-gap:8px;row-gap:8px;padding:2px 0;font-size:13px;line-height:20px} +.bt-turn-error-dot{width:6px;height:6px;margin-top:7px;border-radius:50%;background:#e02135} @@ -17954,14 +16900,22 @@ diff --git a/client.js b/client.js + ).then((outcome2) => { + if (live.current) setState({ busy: false, outcome: outcome2 }); + }); -+ }; + }; +- const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; +- return (0, import_react15.createElement)( + const { busy, outcome } = state; + return (0, import_react24.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, +- showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, +- (0, import_react15.createElement)( + { className: "bt-set-row bt-bs-row bt-model-test" }, + (0, import_react24.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "span", + { className: "bt-set-copy" }, +- (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), +- task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null + (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), + (0, import_react24.createElement)( + "span", @@ -17969,19 +16923,25 @@ diff --git a/client.js b/client.js + busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, + outcome?.code ? (0, import_react24.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null + ) -+ ), + ), +- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), +- (0, import_react15.createElement)("button", { +- type: "button", +- className: "bt-mini bt-task-remove", +- "data-confirm": confirming || void 0, +- disabled: busy, +- "aria-label": confirming ? t("Click again to delete") : t("Delete task"), +- title: confirming ? void 0 : t("Delete task"), +- onClick: remove +- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ); } --function AutomationOff() { -- return (0, import_react15.createElement)( -- "div", -- { className: "bt-set-card bt-task-off" }, -- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), -- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -- ); +-var pad = (number) => String(number).padStart(2, "0"); +-function inAnHour() { +- const date = new Date(Date.now() + 36e5); +- date.setMinutes(0, 0, 0); +- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; +var MODEL_TEST_CSS = ` +.bt-model-test-result{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +.bt-model-test-result:empty{display:none} @@ -17996,14 +16956,36 @@ diff --git a/client.js b/client.js + .bt-model-test{flex-wrap:wrap;row-gap:6px} + .bt-model-test>.bt-set-copy{flex:1 1 100%} } --function SchedulesPage({ roster, actions, start }) { -- const view = useSchedules(actions); -- const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); -- if (view === null) return (0, import_react15.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -- if (!view.available) return (0, import_react15.createElement)(AutomationOff); -- const shown = (next) => { -- publish(next); -- setAdding(false); +-var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; +-var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]]; +-function TaskForm({ roster, actions, start, onDone, onCancel }) { +- const [target, setTarget] = (0, import_react15.useState)(start ?? roster.bots[0]?.id ?? ""); +- const [title, setTitle] = (0, import_react15.useState)(""); +- const [prompt, setPrompt] = (0, import_react15.useState)(""); +- const [kind, setKind] = (0, import_react15.useState)("daily"); +- const [at, setAt] = (0, import_react15.useState)(inAnHour); +- const [time, setTime] = (0, import_react15.useState)("09:00"); +- const [weekdays, setWeekdays] = (0, import_react15.useState)([1, 2, 3, 4, 5]); +- const [count, setCount] = (0, import_react15.useState)(1); +- const [unit, setUnit] = (0, import_react15.useState)("hours"); +- const [busy, setBusy] = (0, import_react15.useState)(false); +- const [error, setError] = (0, import_react15.useState)(""); +- const timing = () => { +- if (kind === "at") return { kind, at: new Date(at).toISOString() }; +- if (kind === "every") return { kind, minutes: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; +- return { kind, time, timeZone: localZone(), ...kind === "weekly" ? { weekdays } : {} }; +- }; +- const submit = (event) => { +- event.preventDefault(); +- if (busy) return; +- setBusy(true); +- setError(""); +- let when3; +- try { +- when3 = timing(); +- } catch { +- setError(t("Pick a date and time")); +- setBusy(false); +`; + +// src/client/folder-menu.js @@ -18022,48 +17004,74 @@ diff --git a/client.js b/client.js + const toggle = () => { + if (isOpen) { + close(); -+ return; -+ } -+ setView("list"); -+ setProblem(""); -+ void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); -+ open(); - }; -- const byChat = [ -- ...roster.bots.map((bot) => [bot.id, (0, import_react15.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), -- ...roster.rooms.map((room2) => [room2.id, (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) -- ].filter(([, , , tasks]) => tasks.length > 0); -- return [ -- adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))), -- byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)( -- "section", -- { key: id, className: "bt-set-section" }, -- (0, import_react15.createElement)("h3", { className: "bt-task-head" }, avatar, name), -- (0, import_react15.createElement)("div", { className: "bt-set-card" }, tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish }))) -- )) -- ]; --} --function BotSchedules({ bot, roster, actions }) { -- const view = useSchedules(actions); -- if (view === null) return null; -- const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; + return; + } +- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { +- setError(hostText(failure?.message ?? String(failure))); +- setBusy(false); +- }); +- }; +- const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); - return (0, import_react15.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), +- "form", +- { className: "bt-set-card bt-task-form", onSubmit: submit }, +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), +- (0, import_react15.createElement)( +- "select", +- { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, +- (0, import_react15.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react15.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), +- roster.rooms.length > 0 ? (0, import_react15.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react15.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null +- ) +- ), +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react15.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) +- ), +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), +- (0, import_react15.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) +- ), - (0, import_react15.createElement)( - "div", -- { className: "bt-set-card" }, -- !view.available ? (0, import_react15.createElement)( +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("When"))), +- (0, import_react15.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react15.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), +- (0, import_react15.createElement)( - "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings."))), -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -- ) : [ -- ...tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish })), -- (0, import_react15.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react15.createElement)(PlusIcon), t("New task")) -- ] -- ) +- { key: kind, className: "bt-task-when bt-veil" }, +- kind === "at" ? (0, import_react15.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, +- kind === "weekly" ? (0, import_react15.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react15.createElement)("button", { +- key: day, +- type: "button", +- className: "bt-task-day", +- "aria-pressed": weekdays.includes(day), +- onClick: () => toggleDay(day) +- }, when2(Date.UTC(2024, 0, day), { weekday: "narrow", timeZone: "UTC" })))) : null, +- kind === "daily" || kind === "weekly" ? (0, import_react15.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, +- kind === "every" ? (0, import_react15.createElement)( +- import_react15.Fragment, +- null, +- (0, import_react15.createElement)("span", { className: "bt-task-every" }, t("Every")), +- (0, import_react15.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), +- (0, import_react15.createElement)( +- "select", +- { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, +- UNITS.map(([id, , label]) => (0, import_react15.createElement)("option", { key: id, value: id }, t(label))) +- ) +- ) : null +- ) ++ setView("list"); ++ setProblem(""); ++ void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); ++ open(); ++ }; + const choose = (path) => { + close(); + if (path !== (value ?? "")) void onPick(path); @@ -18093,7 +17101,14 @@ diff --git a/client.js b/client.js + { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, + (0, import_react25.createElement)("span", { className: "bt-menu-label" }, fallback), + (0, import_react25.createElement)(Tick, { on: !value }) -+ ), + ), +- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, +- (0, import_react15.createElement)( +- "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) +- ) + folders.length > 0 ? (0, import_react25.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, + folders.map((folder) => (0, import_react25.createElement)( + "button", @@ -18110,12 +17125,61 @@ diff --git a/client.js b/client.js + } }, (0, import_react25.createElement)(PencilIcon), (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) ); -} --function ScheduleSummary({ bot, actions }) { -- const view = useSchedules(actions); -- const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; +-function AutomationOff() { - return (0, import_react15.createElement)( + const typeView = (0, import_react25.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-task-off" }, +- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), +- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) +- ); +-} +-function SchedulesPage({ roster, actions, start }) { +- const view = useSchedules(actions); +- const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); +- if (view === null) return (0, import_react15.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); +- if (!view.available) return (0, import_react15.createElement)(AutomationOff); +- const shown = (next) => { +- publish(next); +- setAdding(false); +- }; +- const byChat = [ +- ...roster.bots.map((bot) => [bot.id, (0, import_react15.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), +- ...roster.rooms.map((room2) => [room2.id, (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) +- ].filter(([, , , tasks]) => tasks.length > 0); +- return [ +- adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))), +- byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)( +- "section", +- { key: id, className: "bt-set-section" }, +- (0, import_react15.createElement)("h3", { className: "bt-task-head" }, avatar, name), +- (0, import_react15.createElement)("div", { className: "bt-set-card" }, tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish }))) +- )) +- ]; +-} +-function BotSchedules({ bot, roster, actions }) { +- const view = useSchedules(actions); +- if (view === null) return null; +- const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; +- return (0, import_react15.createElement)( +- "section", +- { className: "bt-set-section" }, +- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), +- (0, import_react15.createElement)( +- "div", +- { className: "bt-set-card" }, +- !view.available ? (0, import_react15.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings."))), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) +- ) : [ +- ...tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish })), +- (0, import_react15.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react15.createElement)(PlusIcon), t("New task")) +- ] +- ) + { key: "type", className: "bt-folder-view" }, + (0, import_react25.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react25.createElement)(ChevronLeftIcon), (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Back"))), + (0, import_react25.createElement)("input", { @@ -18131,7 +17195,12 @@ diff --git a/client.js b/client.js + } + }), + (0, import_react25.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) -+ ); + ); +-} +-function ScheduleSummary({ bot, actions }) { +- const view = useSchedules(actions); +- const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; +- return (0, import_react15.createElement)( + const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; + const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; + const browseView = listing ? (0, import_react25.createElement)( @@ -21341,9 +20410,7 @@ diff --git a/client.js b/client.js "div", - { className: "bt-bs" }, - (0, import_react23.createElement)( -+ { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end }, -+ editing ? (0, import_react37.createElement)( - "div", +- "div", - { className: "bt-bs-hero" }, - (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - (0, import_react23.createElement)( @@ -21369,27 +20436,94 @@ diff --git a/client.js b/client.js - ), - (0, import_react23.createElement)( - Card, -- { title: t("Who answers") }, -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": mode === id, -- disabled: id === "admin" && !admin, -- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, -- onClick: () => { -- if (mode !== id) void update({ mode: id }); -- } -- }, t(label)))), -- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -- ) +- { title: t("Who answers") }, +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { +- key: id, +- type: "button", +- "aria-pressed": mode === id, +- disabled: id === "admin" && !admin, +- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, +- onClick: () => { +- if (mode !== id) void update({ mode: id }); +- } +- }, t(label)))), +- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Working folder") }, +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Members · {count}", { count: members.length }) }, +- members.map((member) => { +- const leads = room2.admin === member.id; +- return (0, import_react23.createElement)( +- "div", +- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, +- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), +- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), +- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null +- ), +- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), +- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null +- ); +- }), +- adding ? [ +- ...outside.map((bot) => (0, import_react23.createElement)( +- "button", +- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { +- setAdding(false); +- void update({ add: [bot.id] }); +- } }, +- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) +- )), +- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) +- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, +- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( +- "span", +- { className: "bt-set-hint" }, +- // The host checks for two members on every change, even a rename. +- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") +- )) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), +- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) - ), - (0, import_react23.createElement)( - Card, -- { title: t("Working folder") }, +- { title: t("Manage") }, - (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) +- ), +- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) ++ { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end }, ++ editing ? (0, import_react37.createElement)( ++ "div", + { className: "bt-wf-palette" }, + (0, import_react37.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), + locals.map((bot) => (0, import_react37.createElement)("button", { @@ -21425,10 +20559,7 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, + (0, import_react37.createElement)( - "div", -- { 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 }) }) ++ "div", + { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, + (0, import_react37.createElement)( + "div", @@ -21551,9 +20682,38 @@ diff --git a/client.js b/client.js + }) + ) ) -+ ) -+ ); -+} + ) + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function Row2({ item, source, onOpen, onRename, onDelete, editable = true }) { + return (0, import_react37.createElement)( + "button", @@ -21569,13 +20729,7 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react37.createElement)("span", null, item.name), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") - ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length }) }, -- members.map((member) => { -- const leads = room2.admin === member.id; -- return (0, import_react23.createElement)( ++ ), + source === "own" && editable ? (0, import_react37.createElement)( + "span", + { className: "bt-set-actions" }, @@ -21616,11 +20770,19 @@ diff --git a/client.js b/client.js + const locked = readOnly !== ""; + const showUse = !locked && (opened?.source === "factory" || opened?.fromFactory !== void 0); + return (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( + { className: "bt-wf-page" }, + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")), + error ? (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + { className: "bt-error", role: "alert" }, + alertCopy(error), + phase === "error" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null @@ -21636,58 +20798,7 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("h3", null, t("Factory presets")), + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")), + (0, import_react37.createElement)( - "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null -- ); -- }), -- adding ? [ -- ...outside.map((bot) => (0, import_react23.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ add: [bot.id] }); -- } }, -- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host checks for two members on every change, even a rename. -- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") -- )) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), -- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Manage") }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -- ), -- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) ++ "div", + { className: "bt-set-card" }, + templates.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => { + const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id); @@ -21707,13 +20818,19 @@ diff --git a/client.js b/client.js + yours.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react37.createElement)(Row2, { key: item.id, item, source: "own", editable: !locked, onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) + ) ) -- ) +- ), +- admin === null ? null : (0, import_react23.createElement)( + ) : null, + graph ? (0, import_react37.createElement)( -+ "div", + "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)( + { className: "bt-wf-editor" }, + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, +- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) + { className: "bt-wf-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), + showUse ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, @@ -21730,51 +20847,17 @@ diff --git a/client.js b/client.js + editing: opened?.source === "own" && editing === true && !locked + }) + ) : null - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react23.useState)(null); -- const [members, setMembers] = (0, import_react23.useState)([]); -- const [name, setName] = (0, import_react23.useState)(""); -- const [busy, setBusy] = (0, import_react23.useState)(false); -- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const pickAdmin = (id) => { -- setAdmin(id); -- setMembers((list) => list.filter((item) => item !== id)); -- }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -- setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -- }; -- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); -- return (0, import_react23.createElement)( ++ ); ++} +function RenameModal({ workflow, onClose, onConfirm }) { + const [name, setName] = (0, import_react37.useState)(workflow?.name ?? ""); + return (0, import_react37.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( ++ "div", + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, + (0, import_react37.createElement)("h3", null, t("Rename workflow")), + (0, import_react37.createElement)( @@ -21784,15 +20867,19 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react37.createElement)( - "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) ++ "div", + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) - ), -- admin === null ? null : (0, import_react23.createElement)( +- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) +- ), +- (0, import_react23.createElement)( + ) + ); +} @@ -21804,32 +20891,18 @@ diff --git a/client.js b/client.js + } }, + (0, import_react37.createElement)( "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, + (0, import_react37.createElement)("h3", null, t("Delete workflow")), + (0, import_react37.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), + (0, import_react37.createElement)( - "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) ++ "div", + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) - ) -- ), -- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ ) ) ); } @@ -23072,10 +22145,8 @@ diff --git a/client.js b/client.js - (0, import_react25.createElement)( + (0, import_react39.createElement)("label", { className: "bt-q-field" }, (0, import_react39.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })), + (0, import_react39.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ "div", ++ { className: "bt-connector-form-row" }, + (0, import_react39.createElement)("span", { className: "bt-connector-link" }), + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send")) + ) @@ -23104,8 +22175,10 @@ diff --git a/client.js b/client.js + })) : (0, import_react39.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react39.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, + "div", + { className: "bt-connector-form-row" }, +- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + (0, import_react39.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, @@ -23181,7 +22254,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 @@ function entrySummary(entry) { +@@ -10270,18 +10270,19 @@ function entrySummary(entry) { if (entry.source === "turn") return failureLabel(entry.code, entry.text); return entry.text; } @@ -23195,6 +22268,7 @@ diff --git a/client.js b/client.js +function FeedbackPage({ actions, run, Section, Row: Row4 }) { + const [what, setWhat] = (0, import_react40.useState)(""); + const [report, setReport] = (0, import_react40.useState)(null); ++ const [allErrors, setAllErrors] = (0, import_react40.useState)(false); + const [preview, setPreview] = (0, import_react40.useState)(false); + const [note, setNote] = (0, import_react40.useState)(""); const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { @@ -23216,7 +22290,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 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10307,69 +10307,79 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { if (!inline && !copied) setPreview(true); })); }; @@ -23227,9 +22301,11 @@ 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 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { + const browserErrors = report ? clientErrors().map((entry) => ({ ...entry, source: "browser" })) : []; + const entries2 = [...report?.entries ?? [], ...browserErrors].sort((a, b) => new Date(b.time) - new Date(a.time)); const total = (report?.total ?? 0) + browserErrors.length; - const shown = entries2.slice(0, 8); +- const shown = entries2.slice(0, 8); ++ const shown = allErrors ? entries2 : entries2.slice(0, 8); return [ - (0, import_react26.createElement)( + (0, import_react40.createElement)( @@ -23305,6 +22381,16 @@ diff --git a/client.js b/client.js )), - (0, import_react26.createElement)( - Row3, ++ entries2.length > 8 ? (0, import_react40.createElement)( ++ "div", ++ { key: "more", className: "bt-set-row bt-fb-more-row" }, ++ (0, import_react40.createElement)("button", { ++ type: "button", ++ className: "bt-soft bt-soft-sm bt-fb-more", ++ "aria-expanded": allErrors, ++ onClick: () => setAllErrors(!allErrors) ++ }, t(allErrors ? "Show less" : "Show more")) ++ ) : null, + (0, import_react40.createElement)( + Row4, { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, diff --git a/patches/ds-bot/files/test/feedback-errors-more.test.js b/patches/ds-bot/files/test/feedback-errors-more.test.js new file mode 100644 index 00000000..b16fafd2 --- /dev/null +++ b/patches/ds-bot/files/test/feedback-errors-more.test.js @@ -0,0 +1,203 @@ +// #2266: the Feedback page's error list reported the whole count in its title +// (`Recent errors · 60`) while drawing eight rows and offering nothing to reach the rest, +// so a person reading a burst of failures could not see the ninth error at all. The list +// now draws the first eight and a `Show more` control that expands it to every loaded row, +// with `Show less` folding it back; a report of eight or fewer errors keeps the list and +// the title exactly as they were, and the control is not drawn. +// +// This test runs the real hooks: `useState` below re-renders the page, `useEffect` runs the +// report load, and a click is the control's own `onClick`. A `useEffect() {}` stub would +// never show the list filling in. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { FeedbackPage } from './src/client/feedback.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) + +const h = (type, props, ...children) => ({ type, props: { ...props, children: children.flat(Infinity) } }) +const all = (node, out = []) => { + if (Array.isArray(node)) { for (const child of node) all(child, out); return out } + if (node && typeof node === 'object') { + out.push(node) + for (const child of [node.props?.children].flat(Infinity)) all(child, out) + } + return out +} + +// A tiny hooks runtime: state slots survive re-renders, setters mark the tree dirty, and +// effects run after the tree is committed (and only when their deps change). +function hooks() { + const states = [] + const effectSlots = [] + let cursor = 0 + let dirty = false + let pending = [] + const sameDeps = (a, b) => (a?.length === b?.length && (a ?? []).every((value, index) => value === b?.[index])) + return { + react: { + createElement: h, + Fragment: Symbol('Fragment'), + Component: class {}, + useState: (initial) => { + const slot = cursor++ + if (!(slot in states)) states[slot] = typeof initial === 'function' ? initial() : initial + const set = (value) => { + const next = typeof value === 'function' ? value(states[slot]) : value + if (next !== states[slot]) { states[slot] = next; dirty = true } + } + return [states[slot], set] + }, + useEffect: (fn, deps) => { + const slot = cursor++ + if (!effectSlots[slot] || !sameDeps(effectSlots[slot].deps, deps)) pending.push({ slot, fn, deps }) + }, + useLayoutEffect: (fn, deps) => { const slot = cursor++; if (!effectSlots[slot] || !sameDeps(effectSlots[slot].deps, deps)) pending.push({ slot, fn, deps }) }, + useMemo: (make) => make(), + useCallback: (fn) => fn, + useRef: (value) => ({ current: value }), + }, + begin() { cursor = 0; pending = [] }, + end() { + for (const { slot, fn, deps } of pending) { + effectSlots[slot]?.cleanup?.() + const cleanup = fn() + effectSlots[slot] = { deps, cleanup: typeof cleanup === 'function' ? cleanup : undefined } + } + pending = [] + }, + get dirty() { return dirty }, + clear() { dirty = false }, + } +} + +const Section = ({ title, children }) => h('section', { className: 'bt-set-section' }, h('h3', null, title), h('div', { className: 'bt-set-card' }, children)) +const Row = ({ label, hint, children }) => h('div', { className: 'bt-set-row' }, + h('div', { className: 'bt-set-copy' }, h('span', null, label), hint ? h('span', { className: 'bt-set-hint' }, hint) : null), + children ? h('div', { className: 'bt-set-control' }, children) : null) + +const T0 = Date.UTC(2026, 10, 3, 4, 0, 0) +const errorEntries = count => Array.from({ length: count }, (_, index) => ({ + time: T0 - index * 1000, + level: 'error', + text: `failure number ${index + 1}`, + source: 'host', +})) + +// The host read caps its payload at fifty entries while `total` counts the whole log, so a +// sixty-error log arrives as fifty rows and a total of sixty — the shape the probe showed. +async function mount({ entries, total } = {}) { + const runtime = hooks() + const report = { + entries, + total, + markdown: '# diagnostics', + issuesUrl: 'https://github.com/ThinkFlowLab/FlyBot/issues/new', + } + const actions = { + diagnostics: () => Promise.resolve(report), + diagnosticsLog: () => Promise.resolve(''), + diagnosticsClear: () => Promise.resolve(), + } + const context = { + module: { exports: {} }, exports: {}, + document: { addEventListener() {}, removeEventListener() {}, activeElement: null, querySelector: () => null }, + navigator: { language: 'en' }, + localStorage: { getItem: () => null, setItem() {} }, + window: { addEventListener() {}, removeEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }) }, + console, setTimeout, clearTimeout, Date, JSON, Promise, Symbol, + __flybot__: { slots: { entries: () => [], subscribe: () => () => {}, version: () => 0 }, call: () => {} }, + require: (name) => { + if (name === 'react') return runtime.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}`) + }, + } + context.module.exports = context.exports + runInNewContext(outputFiles[0].text, context) + const { FeedbackPage } = context.module.exports + + // The renderer's one job: call the function components (Section, Row) the page hands + // its children to, so the drawn tree is what the browser would get. + const expand = (node) => { + if (Array.isArray(node)) return node.map(expand) + if (!node || typeof node !== 'object') return node + const children = [node.props?.children].flat(Infinity).map(expand) + if (typeof node.type === 'function') return expand(node.type({ ...node.props, children })) + return { type: node.type, props: { ...node.props, children } } + } + let tree = null + const render = () => { + runtime.begin() + tree = expand(FeedbackPage({ actions, run: fn => fn, Section, Row })) + runtime.end() + } + const settle = async () => { + for (let step = 0; step < 20; step += 1) { + runtime.clear() + render() + await new Promise(resolve => setTimeout(resolve, 0)) + if (!runtime.dirty) return + } + throw new Error('the page never settled') + } + + await settle() + const find = predicate => all(tree).find(predicate) + const rows = () => all(tree).filter(node => node.props?.className === 'bt-set-row bt-fb-entry') + const more = () => find(node => typeof node.props?.className === 'string' && node.props.className.split(' ').includes('bt-fb-more')) + const text = (node) => [node?.props?.children].flat(Infinity).filter(child => typeof child === 'string' || typeof child === 'number').join('') + const deepText = (node) => all(node).map(part => text(part)).filter(Boolean).join(' ') + const heading = () => find(node => node.type === 'h3' && text(node).startsWith('Recent errors')) + return { + get rows() { return rows() }, + get more() { return more() }, + get heading() { return heading() }, + text, deepText, + title: () => text(heading()), + async click() { more().props.onClick({}); await settle() }, + } +} + +test('#2266: a sixty-error report draws eight rows and offers the rest', async () => { + const page = await mount({ entries: errorEntries(50), total: 60 }) + assert.equal(page.title(), 'Recent errors · 60', 'the title still reports the whole log') + assert.equal(page.rows.length, 8, 'the list still opens with eight rows') + assert.ok(page.more, 'the list offers a way to see the remaining errors') + assert.equal(page.more.props['aria-expanded'], false, 'the control says the rest is not drawn yet') + assert.equal(page.text(page.more).trim(), 'Show more') +}) + +test('#2266: Show more draws the errors past the eighth, and Show less folds them back', async () => { + const page = await mount({ entries: errorEntries(50), total: 60 }) + await page.click() + assert.equal(page.rows.length, 50, 'every loaded error is drawn after the click') + const texts = page.rows.map(row => page.deepText(row)) + assert.ok(texts.some(value => value.includes('failure number 9')), 'the ninth error is reachable') + assert.equal(page.more.props['aria-expanded'], true) + assert.equal(page.text(page.more).trim(), 'Show less') + await page.click() + assert.equal(page.rows.length, 8, 'Show less folds the list back to eight rows') + assert.equal(page.text(page.more).trim(), 'Show more') +}) + +test('#2266: eight or fewer errors draw no control and keep the title as it was', async () => { + const page = await mount({ entries: errorEntries(3), total: 3 }) + assert.equal(page.title(), 'Recent errors · 3') + assert.equal(page.rows.length, 3) + assert.equal(page.more, undefined, 'nothing to expand means no control') + + const exact = await mount({ entries: errorEntries(8), total: 8 }) + assert.equal(exact.title(), 'Recent errors · 8') + assert.equal(exact.rows.length, 8) + assert.equal(exact.more, undefined, 'exactly eight rows are all drawn, so no control') +})