diff --git a/.github/scripts/ds-bot-patches.test.mjs b/.github/scripts/ds-bot-patches.test.mjs index 7eb4bbd4e..bf715adf6 100644 --- a/.github/scripts/ds-bot-patches.test.mjs +++ b/.github/scripts/ds-bot-patches.test.mjs @@ -193,11 +193,12 @@ if (gitAvailable) { const root = join(import.meta.dirname, '..', '..') const patchesDir = join(root, 'patches', 'ds-bot') const stored = readdirSync(patchesDir).filter(name => /^\d{4}-.*\.patch$/.test(name)).sort() - // 11: `0017-task-pool-role-queues.patch` only edited whole-file mirrors - // (`src/host/tasks.js`, `src/host/hooks.js`) plus one `tools.js` line that lives in `0002`, - // so the generator folds it away instead of storing a patch that applies nothing. The count is - // 11 since `0018-project-dossier.patch`, which edits upstream files and so is stored. - assert.equal(stored.length, 11) + // 10: `0017-task-pool-role-queues.patch` only edited whole-file mirrors + // (`src/host/tasks.js`, `src/host/hooks.js`) plus one `tools.js` line that lives in `0002`, so + // the generator folds it away instead of storing a patch that applies nothing. `0018` (#1883) + // goes the same way once the generator runs: its `client.js` hunk moves into `0008` and its + // other edits into their own topics, so a regenerated series stores 10 patches. + assert.equal(stored.length, 10) const viaGit = mkdtempSync(join(tmpdir(), 'patch-git-')) const viaOurs = mkdtempSync(join(tmpdir(), 'patch-ours-')) try { diff --git a/changelog.d/1887-memory-counter.fixed.md b/changelog.d/1887-memory-counter.fixed.md new file mode 100644 index 000000000..67c27c137 --- /dev/null +++ b/changelog.d/1887-memory-counter.fixed.md @@ -0,0 +1 @@ +The Project settings memory box warns about its bound before the click: a `N/600` counter under the field counts code points like the host, a 601st code point turns the counter and the Save button into their over-limit state, and an empty or whitespace-only box disables Save, so neither state calls `project-memory-write` or writes anything ([#1887](https://github.com/ThinkFlowLab/FlyBot/issues/1887)). diff --git a/core/client.js b/core/client.js index c86611a3f..fc19713fe 100644 --- a/core/client.js +++ b/core/client.js @@ -10464,6 +10464,10 @@ var PROJECT_BOARD_CSS = ` .bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} .bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} .bt-project-field textarea{min-height:88px;resize:vertical} +/* #1887: the counter under the memory box. It stays quiet until the text passes the bound, and + past it turns the one colour that says so; the disabled Save is the state a reader acts on. */ +.bt-project-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums} +.bt-project-count[data-over="true"]{opacity:1;color:#c2410c} .bt-project-row-actions{display:flex;gap:8px;align-items:center} .bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} .bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} @@ -10502,6 +10506,8 @@ var PROJECT_BOARD_CSS = ` var PROJECT_SOURCES = ["bot", "group", "workflow"]; var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; var text = (value) => typeof value === "string" ? value.trim() : ""; +var MEMORY_TEXT_MAX = 600; +var memoryLength = (value) => [...text(value)].length; var DONE_COLUMN = "done"; var boardOfRow = (row, task) => { if (task === void 0) return null; @@ -11132,6 +11138,9 @@ function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, const [kind, setKind] = (0, import_react19.useState)("bot"); const [memberId, setMemberId] = (0, import_react19.useState)(""); const [note, setNote] = (0, import_react19.useState)(""); + const noteLength = memoryLength(note); + const noteOver = noteLength > MEMORY_TEXT_MAX; + const noteSaveDisabled = text(note) === "" || noteOver; const [assistantOff, setAssistantOff] = (0, import_react19.useState)(assistantMine === void 0 ? project?.assistant?.off === true : assistantMine?.off === true); const [status, setStatus] = (0, import_react19.useState)(""); const id = text(project?.id); @@ -11186,10 +11195,22 @@ function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, // never as a third kind the host would refuse (#1820). (0, import_react19.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")), (0, import_react19.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }), + (0, import_react19.createElement)("div", { className: "bt-project-count", "data-role": "project-memory-count", "data-over": noteOver ? "true" : "false" }, `${noteLength}/${MEMORY_TEXT_MAX}`), (0, import_react19.createElement)( "div", { className: "bt-project-row-actions" }, - (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory")) + (0, import_react19.createElement)("button", { + type: "button", + className: "bt-soft bt-soft-sm", + "data-action": "project-memory-save", + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. + onClick: () => { + if (noteSaveDisabled) return; + run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")); + } + }, t("Save memory")) ) ), (0, import_react19.createElement)("div", { className: "bt-project-status-line", role: "status" }, status), diff --git a/core/src/client/project-board.js b/core/src/client/project-board.js index 73fdd5649..41a062d6a 100644 --- a/core/src/client/project-board.js +++ b/core/src/client/project-board.js @@ -81,6 +81,10 @@ export const PROJECT_BOARD_CSS = ` .bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} .bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} .bt-project-field textarea{min-height:88px;resize:vertical} +/* #1887: the counter under the memory box. It stays quiet until the text passes the bound, and + past it turns the one colour that says so; the disabled Save is the state a reader acts on. */ +.bt-project-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums} +.bt-project-count[data-over="true"]{opacity:1;color:#c2410c} .bt-project-row-actions{display:flex;gap:8px;align-items:center} .bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} .bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} @@ -124,6 +128,14 @@ export const SOURCE_LABELS = { bot: 'Bots', group: 'Groups', workflow: 'Workflow const text = value => (typeof value === 'string' ? value.trim() : '') +// #1887: the memory box warns about the bound before the click, the way the host bounds it. One +// entry is at most `MAX_ENTRY` (`src/host/constants.js`, #1847) and the host counts its code points +// (#1886), so the counter counts the same unit on the trimmed text the host would store — which is +// also why the field carries no `maxLength`: that counts UTF-16 units and would call a 600-emoji +// entry twice its real length. +export const MEMORY_TEXT_MAX = 600 +const memoryLength = (value) => [...text(value)].length + // #1855: the column a finished card reads. The host sends those cards apart (`done`) and the view // keeps them out of every group of work in flight. const DONE_COLUMN = 'done' @@ -753,6 +765,11 @@ export function ProjectSettingsPanel({ project, assistantMine, call: request, on const [kind, setKind] = useState('bot') const [memberId, setMemberId] = useState('') const [note, setNote] = useState('') + // #1887: an empty box has nothing to save and an over-long one can never land, so neither + // reaches the host — the reader sees the state before the click instead of a refusal after it. + const noteLength = memoryLength(note) + const noteOver = noteLength > MEMORY_TEXT_MAX + const noteSaveDisabled = text(note) === '' || noteOver // #1845: the digest opt-out is per owner, and the host keeps it on `owners[]`; the // Project record has no such field, so reading it there showed a paused digest as unticked and // the next Save turned it back on. `project-read` answers the caller's own record beside the @@ -794,8 +811,20 @@ export function ProjectSettingsPanel({ project, assistantMine, call: request, on // never as a third kind the host would refuse (#1820). h('label', { htmlFor: 'bt-project-note' }, t('Add to the assistant memory')), h('textarea', { id: 'bt-project-note', value: note, onChange: event => setNote(event.target.value) }), + h('div', { className: 'bt-project-count', 'data-role': 'project-memory-count', 'data-over': noteOver ? 'true' : 'false' }, `${noteLength}/${MEMORY_TEXT_MAX}`), h('div', { className: 'bt-project-row-actions' }, - h('button', { type: 'button', className: 'bt-soft bt-soft-sm', 'data-action': 'project-memory-save', onClick: () => run('project-memory-write', { id, kind: 'decision', text: text(note) }, t('Memory saved.')) }, t('Save memory')))), + h('button', { + type: 'button', + className: 'bt-soft bt-soft-sm', + 'data-action': 'project-memory-save', + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. + onClick: () => { + if (noteSaveDisabled) return + run('project-memory-write', { id, kind: 'decision', text: text(note) }, t('Memory saved.')) + }, + }, t('Save memory')))), h('div', { className: 'bt-project-status-line', role: 'status' }, status), h('div', { className: 'bt-project-row-actions' }, h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: () => onClose?.() }, t('Back to board')))) diff --git a/core/test/project-board-panels.test.js b/core/test/project-board-panels.test.js index 0ed3f0d5c..0de1b4e01 100644 --- a/core/test/project-board-panels.test.js +++ b/core/test/project-board-panels.test.js @@ -304,6 +304,9 @@ test('a memory kind the host does not know writes nothing, and the panel quotes try { const refused = async () => { throw Object.assign(new Error('Only the owner of this Project may change it'), { code: 'bot/forbidden' }) } const draw = () => render(ProjectSettingsPanel, { project: project(), call: refused, onClose() {} }) + // #1887: an empty box no longer reaches the host, so the entry is one the panel allows; the + // refusal below is the owner gate, which is what this half of the test is about. + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: 'ship on Friday' } }) let tree = draw() byAction(tree, 'project-memory-save').props.onClick() assert.equal(await statusAfter(draw, '只有这个项目的主人可以修改'), '只有这个项目的主人可以修改') @@ -345,36 +348,29 @@ test('#1847: the memory write door refuses empty and over-long text and stores 6 assert.equal(memory.decisions[0].text, exact, 'the text is what was sent') }) -// The other half of #1847: the panel quotes the host's new refusals in Chinese, so the reader sees -// why Save did nothing instead of a success line, and no row is added. -test('#1847: the panel shows the Chinese refusal for an empty or over-long memory box', async (t) => { +// The other half of #1847: the panel quotes the host's refusals in Chinese. Since #1887 the panel +// refuses an empty or over-long box before the click, so the one refusal still reachable is the two +// bounds disagreeing — an emoji entry the client counts as exactly 600 code points while a host +// that still counted UTF-16 units (#1886) answers "at most 600 characters". The reader must see +// why Save did nothing, never a success line, and no row may land. +test('#1847: the panel shows the Chinese refusal when the host refuses the memory text', async (t) => { + resetSlots() const rt = await startHost(t) const call = (endpoint, payload) => rt.projectEndpoint(endpoint, payload) const off = installLocale(fakeLocale('zh')) try { - // The hook slots outlive a render, so the box starts where the previous test left it; empty it - // explicitly before pressing Save. - let tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - all(tree).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: '' } }) - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - byAction(tree, 'project-memory-save').props.onClick() - await settle() - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - let status = all(tree).find(node => node.props?.role === 'status') - assert.equal(text(status), '项目记忆条目需要文字') - assert.ok(!text(status).includes('Memory saved.'), 'a refusal is never painted as a success') - - const note = all(tree).find(node => node.props?.id === 'bt-project-note') - note.props.onChange({ target: { value: 'x'.repeat(601) } }) - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - byAction(tree, 'project-memory-save').props.onClick() - await settle() - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - status = all(tree).find(node => node.props?.role === 'status') - assert.equal(text(status), '项目记忆条目最多 600 个字符') + const draw = () => render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: '🙂'.repeat(600) } }) + const refused = Object.assign(new Error('A Project memory entry is at most 600 characters'), { code: 'bot/invalid' }) + const racing = () => render(ProjectSettingsPanel, { project: project(), call: async () => { throw refused }, onClose() {} }) + assert.ok(!byAction(racing(), 'project-memory-save').props.disabled, 'the client counts the entry at the bound and lets the click through') + byAction(racing(), 'project-memory-save').props.onClick() + assert.equal(await statusAfter(racing, '项目记忆条目最多 600 个字符'), '项目记忆条目最多 600 个字符') + assert.ok(!drawnStatus(racing).includes('记忆已保存。'), 'a refusal is never painted as a success') + assert.equal(hostText('A Project memory entry needs text'), '项目记忆条目需要文字', 'the empty-text refusal is translated too, for a host that refuses it') const memory = (await call('project-memory', { id: 'project-1' })).memory - assert.deepEqual(memory.decisions, [], 'neither refusal adds a row') + assert.deepEqual(memory.decisions, [], 'the refusal adds no row') } finally { off() } @@ -385,6 +381,8 @@ test("the settings panel shows the host's refusal instead of pretending the save const refused = Object.assign(new Error('Only the owner of this Project may change it'), { code: 'bot/forbidden' }) const save = async () => { throw refused } const draw = () => render(ProjectSettingsPanel, { project: { id: 'project-1' }, call: save, onClose() {} }) + // #1887: the box needs an entry the panel allows, or Save is disabled and no host refusal is asked. + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: 'ship on Friday' } }) const tree = draw() byAction(tree, 'project-memory-save').props.onClick() await waitFor(() => (/Only the owner of this Project/.test(drawnStatus(draw)) ? true : undefined)) @@ -536,3 +534,53 @@ test('the owner\'s chat entry opens the Project assistant, and only the owner\'s assert.equal(projectAgentFor({ ownerId: 'owner-1' }, 'owner-1'), null, 'a Bot outside a Project offers no entry') assert.equal(projectAgentFor({ projectId: 'project-1', ownerId: 'owner-1', remote: {} }, 'owner-1'), null, "a friend's Bot offers no entry") }) + +// #1887: the memory box warns before the click. The field had no counter and Save stayed enabled +// with nothing typed, so the first news of the 600 bound was the host's refusal after the click +// (#1847 already refuses at the door — the defect is the silence before it, not a blank entry +// landing). The counter counts code points, the unit the host bounds (#1886), so an emoji entry at +// the bound is not refused for what `maxLength` would have counted as twice as many UTF-16 units. +const memoryCount = (tree) => all(tree).find(node => node.props?.['data-role'] === 'project-memory-count') +const memorySave = (tree) => byAction(tree, 'project-memory-save') + +test('#1887: the memory box counts code points and disables Save before the click', async (t) => { + resetSlots() + const rt = await startHost(t) + let calls = 0 + const call = (endpoint, payload) => { calls += 1; return rt.projectEndpoint(endpoint, payload) } + const draw = () => render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) + const type = (value) => { + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value } }) + return draw() + } + + // Empty: there is nothing to save, and the reader is told before pressing anything. + let tree = type('') + assert.equal(memorySave(tree).props.disabled, true, 'an empty memory box disables Save') + assert.equal(text(memoryCount(tree)), '0/600', 'the counter reads the empty box') + memorySave(tree).props.onClick() + await settle() + + // Whitespace is not an entry either: the host refuses it as no text, so the button agrees. + tree = type(' \n ') + assert.equal(memorySave(tree).props.disabled, true, 'a whitespace-only memory box disables Save') + + // One past the bound: the counter says so and Save is disabled before the click. + const emoji = '🙂' + tree = type(emoji.repeat(601)) + assert.equal(text(memoryCount(tree)), '601/600', 'the counter counts code points, not UTF-16 units') + assert.equal(memoryCount(tree).props['data-over'], 'true', 'the counter is in its over-limit state') + assert.equal(memorySave(tree).props.disabled, true, 'an over-long entry disables Save') + memorySave(tree).props.onClick() + await settle() + + // The bound itself is an entry: Save is enabled and the counter reads it whole. + tree = type(emoji.repeat(600)) + assert.equal(text(memoryCount(tree)), '600/600', 'an entry at the bound reads the bound') + assert.equal(memoryCount(tree).props['data-over'], 'false', 'and the counter is not in its over-limit state') + assert.ok(!memorySave(tree).props.disabled, 'the bound itself is allowed') + + assert.equal(calls, 0, 'neither a disabled nor an over-limit Save calls the endpoint') + const memory = (await rt.projectEndpoint('project-memory', { id: 'project-1' })).memory + assert.deepEqual(memory.decisions, [], 'and neither state writes anything') +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 2edca2a5f..967e366d6 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -291,9 +291,13 @@ the two kinds the host knows: the free text is recorded as a `decision`, never a quotes a refusal through `hostText`, so a zh reader reads the host's own sentence in Chinese: since #1847 the host refuses an empty entry and one over 600 characters at that write door, and `i18n.js` carries both sentences — the exact key for `A Project memory entry needs text` and the number-carrying -`[/^A Project memory entry is at most (\d+) characters$/, …]` — so a zh box that was empty or over the -cap reads 项目记忆条目需要文字 / 项目记忆条目最多 600 个字符 instead of `Memory saved.`, with no row -added. Tests: `core/test/project-board-panels.test.js`. +`[/^A Project memory entry is at most (\d+) characters$/, …]` — so a zh box the host refuses reads +项目记忆条目需要文字 / 项目记忆条目最多 600 个字符 instead of `Memory saved.`, with no row added. Since +#1887 the box warns before that click: a counter under the field reads `N/600` in code points, the unit +the host bounds (#1886) and not what `maxLength` would count in UTF-16 units, it turns over-limit at 601, +and an empty or whitespace-only box disables Save — neither state calls the endpoint or writes anything, +so the two host sentences stay for the race where the host refuses an entry the panel allowed. Tests: +`core/test/project-board-panels.test.js`. The **Members** panel lists every owner and every member the board names, and since #1859 also the Project's own Chief as its own row (`data-member-role="chief"`), read from `chief` on the `project-read` diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index 12ea4618d..f6e173223 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -1447,7 +1447,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Working folder': '工作文件夹', 'Working folder: {folder}': '工作文件夹:{folder}', Folder: '文件夹', -@@ -838,6 +838,77 @@ const ZH = { +@@ -838,6 +838,86 @@ const ZH = { 'Up one level': '上一级', 'No folders inside.': '里面没有文件夹。', 'Some folders are not shown.': '还有一些文件夹没有显示。', @@ -1520,6 +1520,15 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js + 'Role pool': '角色池', + 'Can switch to {roles}': '可切换为 {roles}', + 'No one in this pool yet.': '角色池里还没有人。', ++ // #1864: the Project team's dossier — persona, memory and skills are read-only in this slice. ++ 'Dossier': '档案', ++ 'Persona': '人设', ++ 'Skills': '技能', ++ 'No memory yet.': '还没有记忆。', ++ 'No skills attached.': '还没有附加技能。', ++ 'Open chat': '打开对话', ++ 'Open my agent': '打开我的项目助手', ++ "Open {name}'s dossier": '打开 {name} 的档案', + 'No cards yet.': '还没有卡片。', + 'Open Projects in Settings': '在设置中打开项目', } @@ -4551,6 +4560,14 @@ diff --git a/src/host/memory.js b/src/host/memory.js 'Reply with one JSON object and nothing else: {"changes": […]}, with an empty list when nothing should change.', `At most ${REVIEW_CHANGES_MAX} changes in one answer, the most lasting first; the rest can wait for the next review.`, 'Keep, as a new entry:', +@@ -408,6 +408,7 @@ export function install(rt) { + memoryEndpoint: async () => { throw new Error('Memory is turned off on this host') }, + memoryNews: () => ({}), + memoryReviews: () => 0, ++ memoryDigest: () => ({ head: '', topics: [] }), + }) + return + } @@ -486,7 +486,24 @@ export function install(rt) { // Main Bots and the admins of a group also write the team's; Main Bots read and // write every Bot's. The user may do everything from the memory panel. @@ -4754,7 +4771,7 @@ diff --git a/src/host/memory.js b/src/host/memory.js } } -@@ -1378,8 +1378,29 @@ export function install(rt) { +@@ -1378,8 +1378,42 @@ export function install(rt) { throw new Error(`Unknown endpoint ${endpoint}`) } @@ -4777,11 +4794,24 @@ diff --git a/src/host/memory.js b/src/host/memory.js + actor: { id: botId, name: bot.name }, label: 'your memory', + }) + } ++ ++ // The Project board's dossier (#1864) reads the head and the topic names only: never an entry ++ // body from a topic, and never another scope. It is a read seam, so the panel adds no second ++ // memory file reader beside this one and cannot drift from where `remember` writes. ++ function memoryDigest(botId, limits = {}) { ++ const scope = ownScope(String(botId ?? '')) ++ const head = Number.isFinite(limits.head) ? limits.head : 4000 ++ const topics = Number.isFinite(limits.topics) ? limits.topics : 64 ++ return { ++ head: readText(join(dirOf(scope), MAIN)).slice(0, head), ++ topics: topicNames(scope).slice(0, topics), ++ } ++ } + Object.assign(rt, { memoryEnabled: true, renewMemory, archiveBotMemory, memoryEndpoint, memoryReviews, memoryNews: () => Object.fromEntries(news), -+ botMemoryRead, botMemoryAppend, ++ botMemoryRead, botMemoryAppend, memoryDigest, }) } diff --git a/src/host/models.js b/src/host/models.js diff --git a/patches/ds-bot/0002-flybot-api-host.patch b/patches/ds-bot/0002-flybot-api-host.patch index 3f3c9d22a..4e1368d11 100644 --- a/patches/ds-bot/0002-flybot-api-host.patch +++ b/patches/ds-bot/0002-flybot-api-host.patch @@ -33,7 +33,7 @@ diff --git a/index.js b/index.js import * as chatIndex from './src/host/chat-index.js' import * as groups from './src/host/groups.js' import * as approvals from './src/host/approvals.js' -@@ -24,11 +24,18 @@ import * as images from './src/host/images.js' +@@ -24,11 +24,19 @@ import * as images from './src/host/images.js' import * as browser from './src/host/browser.js' import * as secrets from './src/host/secrets.js' import * as connectors from './src/host/connectors.js' @@ -42,6 +42,7 @@ diff --git a/index.js b/index.js import * as schedules from './src/host/schedules.js' +import * as projectAssistant from './src/host/project-assistant.js' +import * as projectChief from './src/host/project-chief.js' ++import * as projectTeam from './src/host/project-team.js' +import * as automationDefault from './src/host/automation-default.js' import * as usage from './src/host/usage.js' import * as update from './src/host/update.js' @@ -57,7 +58,7 @@ diff --git a/index.js b/index.js // install. Listeners run and tools are listed in registration order, so this order holds: // usage meters model requests after every listener that rewrites them. -const MODULES = [diagnostics, store, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, chatIndex, groups, approvals, resume, memory, parts, images, secrets, connectors, capabilities, schedules, browser, usage, update, dshUpdate, channel] -+const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel] ++const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, projectTeam, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel] export function apply(ctx, config = {}) { - createRuntime(ctx, config) @@ -896,7 +897,7 @@ diff --git a/src/host/store.js b/src/host/store.js // LEGACY: Bots saved with a lab ran on that lab's default model; the model they // last ran on becomes their own, so dropping the lab changes nothing for them. if ('lab' in bot) { -@@ -161,12 +161,472 @@ export function install(rt) { +@@ -161,12 +161,476 @@ export function install(rt) { migrated = true } // Unnamed groups from before names followed their members may still list old names. @@ -1308,6 +1309,10 @@ diff --git a/src/host/store.js b/src/host/store.js + return { project: readProject(project.id), ...(result ?? {}) } + } + case 'project-of': return { project: projectOfMember(projectText(body.kind, 20), projectText(body.id, 200)) } ++ // The read-only team dossier (#1864): the Project's Chief, its assistant and its pool, each ++ // with persona, memory head/topics and skills. The read lives in project-team.js; a missing ++ // Bot is a `null` row there, never an error for the whole answer. ++ case 'project-team': return rt.projectTeam?.(projectText(body.id, 200)) ?? { rows: [] } + // The read-only board (#1696): Project id in, its tasks out. The aggregate lives in + // project-board.js; it reads and returns, and writes nothing. The actor goes with the id + // so the board applies the same visibility rule `tasks` does (#1723). diff --git a/patches/ds-bot/0005-social-seams.patch b/patches/ds-bot/0005-social-seams.patch index 49871a459..22468387f 100644 --- a/patches/ds-bot/0005-social-seams.patch +++ b/patches/ds-bot/0005-social-seams.patch @@ -841,7 +841,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js content: [{ type: 'text', text }], }, AbortSignal.timeout(30_000)) return ok(true) -@@ -319,32 +319,134 @@ export function install(rt) { +@@ -319,32 +319,135 @@ export function install(rt) { case 'diagnostics-clear': await rt.diagnostics.clear() return ok(true) @@ -871,6 +871,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js + case 'project-bind': + case 'project-of': + case 'project-board': ++ case 'project-team': + case 'project-role': + case 'project-delete': + case 'project-memory': diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index b55caa8f6..272b77d9c 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -866,7 +866,7 @@ diff --git a/client.js b/client.js "Working folder": "工作文件夹", "Working folder: {folder}": "工作文件夹:{folder}", Folder: "文件夹", -@@ -887,7 +887,78 @@ var ZH = { +@@ -887,7 +887,87 @@ var ZH = { "Use this folder": "使用这个文件夹", "Up one level": "上一级", "No folders inside.": "里面没有文件夹。", @@ -941,6 +941,15 @@ diff --git a/client.js b/client.js + "Role pool": "角色池", + "Can switch to {roles}": "可切换为 {roles}", + "No one in this pool yet.": "角色池里还没有人。", ++ // #1864: the Project team's dossier — persona, memory and skills are read-only in this slice. ++ "Dossier": "档案", ++ "Persona": "人设", ++ "Skills": "技能", ++ "No memory yet.": "还没有记忆。", ++ "No skills attached.": "还没有附加技能。", ++ "Open chat": "打开对话", ++ "Open my agent": "打开我的项目助手", ++ "Open {name}'s dossier": "打开 {name} 的档案", + "No cards yet.": "还没有卡片。", + "Open Projects in Settings": "在设置中打开项目" }; @@ -4964,31 +4973,10 @@ diff --git a/client.js b/client.js @keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} .bt-mem-skel{display:flex;flex-direction:column;gap:24px} .bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} -@@ -2574,2206 +2574,4679 @@ 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}} - --.bt-thumb{position:absolute;left:0;top:0;z-index:0;pointer-events:none;transition:opacity .14s ease} --.bt-thumb[data-hidden]{opacity:0} --[data-morph]{overflow:hidden!important} --.bt-veil{animation:bt-veil-in .22s ease-out} --.bt-with-icon{display:inline-flex;align-items:center;gap:4px} --.bt-with-icon svg{width:14px;height:14px} --.bt-tag-admin{display:inline-flex;align-items:center;gap:4px} --.bt-create-inline{padding:18px 16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} --.bt-create-inline .bt-create-card{max-width:none} --.bt-panel-view>[data-move=in]{animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-panel-view>[data-move=out]{animation:bt-page-out ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(2){animation-delay:25ms} --.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(n+3){animation-delay:50ms} --.bt-panel-view>.bt-drawer-body[data-move=tab]{animation:bt-veil-in .2s ease-out} --.bt-drawer-name-input{animation:bt-veil-in .16s ease-out} --.bt-pop-in{display:inline-flex;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --:is(.bt-soft,.bt-send,.bt-icon-btn,.bt-settings-close,.bt-mini,.bt-tab,.bt-nc-send,.bt-avedit-tab,.bt-shape,.bt-pick,.bt-swatch,.bt-palette-clear){transition:background-color .12s ease,color .12s ease,border-color .12s ease,opacity .12s ease,${spring(SPRING_TAP, "scale")}} --:is(.bt-icon-btn,.bt-settings-close,.bt-nc-send,.bt-shape,.bt-swatch,.bt-color,.bt-x,.bt-q-x,.bt-palette-clear):active:not(:disabled){scale:.9} --:is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} --@media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} --`; +@@ -2595,2185 +2595,4658 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} + :is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} + @media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} + `; -var CONVERSATION_CSS = ` -.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} -.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} @@ -5069,27 +5057,6 @@ diff --git a/client.js b/client.js -.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)} -[data-bt-enter="new"]{animation:none} -[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out} -+.bt-thumb{position:absolute;left:0;top:0;z-index:0;pointer-events:none;transition:opacity .14s ease} -+.bt-thumb[data-hidden]{opacity:0} -+[data-morph]{overflow:hidden!important} -+.bt-veil{animation:bt-veil-in .22s ease-out} -+.bt-with-icon{display:inline-flex;align-items:center;gap:4px} -+.bt-with-icon svg{width:14px;height:14px} -+.bt-tag-admin{display:inline-flex;align-items:center;gap:4px} -+.bt-create-inline{padding:18px 16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} -+.bt-create-inline .bt-create-card{max-width:none} -+.bt-panel-view>[data-move=in]{animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} -+.bt-panel-view>[data-move=out]{animation:bt-page-out ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} -+.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(2){animation-delay:25ms} -+.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(n+3){animation-delay:50ms} -+.bt-panel-view>.bt-drawer-body[data-move=tab]{animation:bt-veil-in .2s ease-out} -+.bt-drawer-name-input{animation:bt-veil-in .16s ease-out} -+.bt-pop-in{display:inline-flex;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+:is(.bt-soft,.bt-send,.bt-icon-btn,.bt-settings-close,.bt-mini,.bt-tab,.bt-nc-send,.bt-avedit-tab,.bt-shape,.bt-pick,.bt-swatch,.bt-palette-clear){transition:background-color .12s ease,color .12s ease,border-color .12s ease,opacity .12s ease,${spring(SPRING_TAP, "scale")}} -+:is(.bt-icon-btn,.bt-settings-close,.bt-nc-send,.bt-shape,.bt-swatch,.bt-color,.bt-x,.bt-q-x,.bt-palette-clear):active:not(:disabled){scale:.9} -+:is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} -+@media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} -+`; +var CONVERSATION_CSS = ` +.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} +.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap} @@ -5671,34 +5638,6 @@ diff --git a/client.js b/client.js + (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }), + (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 }) + ); -+} -+ -+// src/client/questions.js -+var import_react6 = require("react"); -+var keyLetter = (index) => String.fromCharCode(65 + index); -+var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); -+var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; -+function useOptionKeys(count, onPick, ownRef) { -+ const pick2 = (0, import_react6.useRef)(onPick); -+ pick2.current = onPick; -+ (0, import_react6.useEffect)(() => { -+ if (count === 0) return void 0; -+ const onKey = (event) => { -+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -+ 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(); -+ } -+ }; -+ document.addEventListener("keydown", onKey, true); -+ return () => document.removeEventListener("keydown", onKey, true); -+ }, [count, ownRef]); } -@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} -@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} @@ -5814,6 +5753,42 @@ diff --git a/client.js b/client.js - style.textContent = text; - document.head.appendChild(style); - return style; ++ ++// src/client/questions.js ++var import_react6 = require("react"); ++var keyLetter = (index) => String.fromCharCode(65 + index); ++var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); ++var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; ++function useOptionKeys(count, onPick, ownRef) { ++ const pick2 = (0, import_react6.useRef)(onPick); ++ pick2.current = onPick; ++ (0, import_react6.useEffect)(() => { ++ if (count === 0) return void 0; ++ const onKey = (event) => { ++ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; ++ 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(); ++ } ++ }; ++ document.addEventListener("keydown", onKey, true); ++ return () => document.removeEventListener("keydown", onKey, true); ++ }, [count, ownRef]); + } +-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 QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { + const [own, setOwn] = (0, import_react6.useState)(""); + const [picked, setPicked] = (0, import_react6.useState)([]); @@ -5925,15 +5900,7 @@ diff --git a/client.js b/client.js + ) : null, + foot + ); - } --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 SettledQuestion({ question, answer }) { + const options = optionsOf(question); + const prompt = question?.question ?? ""; @@ -5981,7 +5948,10 @@ diff --git a/client.js b/client.js + return; } + pending.answer({ answers: next }); -+ }; + }; +- try { +- visit(sheet.cssRules); +- } catch { + return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +} +function PendingQuestion({ sessionId, question, actions, inline }) { @@ -5990,10 +5960,7 @@ diff --git a/client.js b/client.js + const onAnswer = (answer) => { + setAnswered(question.id); + void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); - }; -- try { -- visit(sheet.cssRules); -- } catch { ++ }; + const onDismiss = () => { + setAnswered(question.id); + void actions.dismissQuestion(sessionId); @@ -6111,6 +6078,19 @@ diff --git a/client.js b/client.js - const schedule = () => { - clearTimeout(timer); - timer = setTimeout(render, 120); +- }; +- const onLoad = (event) => { +- if (event.target instanceof HTMLLinkElement) schedule(); +- }; +- const observer = new MutationObserver(schedule); +- observer.observe(document.head, { childList: true }); +- document.addEventListener("load", onLoad, true); +- return () => { +- observer.disconnect(); +- document.removeEventListener("load", onLoad, true); +- clearTimeout(timer); +- style.remove(); +- }; +function ScopeSwitch({ value, onChange, bot }) { + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react7.createElement)( @@ -6126,7 +6106,44 @@ diff --git a/client.js b/client.js + onClick: () => onChange(id) + }, text2)) + ); -+} + } +- +-// 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 SecretHead({ bot, title, detail, name, onClose }) { + return (0, import_react7.createElement)( + "div", @@ -6140,7 +6157,32 @@ diff --git a/client.js b/client.js + ), + onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null + ); -+} + } +-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 SecretCard({ bot, request, roster, actions }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); @@ -6154,24 +6196,17 @@ diff --git a/client.js b/client.js + setError(""); + return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; -- const onLoad = (event) => { -- if (event.target instanceof HTMLLinkElement) schedule(); +- 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) } })); + 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 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 allow = () => { + if (!busy) void run(actions.secretAllow(request.id)); - }; ++ }; + const filling = request.kind === "fill" || replacing; + const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; + return (0, import_react7.createElement)( @@ -6210,43 +6245,11 @@ diff --git a/client.js b/client.js + ) + ); } -- --// 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 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 SettledSecret({ bot, request, roster }) { + const canceled = request.status === "canceled"; + const text2 = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); @@ -6265,11 +6268,19 @@ diff --git a/client.js b/client.js + )), + canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) + ) -+ ); -+} + ); + } +-function 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 RequestView({ bot, request, roster, actions }) { + return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster }); -+} + } +-function 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 SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { + const bot = roster.byId[sessionId]; + if (!bot) return null; @@ -6279,22 +6290,50 @@ diff --git a/client.js b/client.js + return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); + } + return null; -+} -+function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { -+ const roster = useRoster((value) => value); -+ const shown = useCards?.((state) => state[sessionId]) ?? NONE; -+ const loading = useSession?.(historyLoading) === true; -+ const bot = roster.byId[sessionId]; -+ if (!bot || currentPart(bot) !== sessionId) return null; -+ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); -+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); -+ if (waiting.length === 0) return null; -+ return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); -+} -+var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; -+function ScopeEditor({ secret, roster, onSave }) { -+ const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); -+ const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope); + } +-function useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; +- } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const roster = useRoster((value) => value); ++ const shown = useCards?.((state) => state[sessionId]) ?? NONE; ++ const loading = useSession?.(historyLoading) === true; ++ const bot = roster.byId[sessionId]; ++ if (!bot || currentPart(bot) !== sessionId) return null; ++ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); ++ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); ++ if (waiting.length === 0) return null; ++ return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); + } +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null ++var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; ++function ScopeEditor({ secret, roster, onSave }) { ++ const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); ++ const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope); + const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const next = mode === "all" ? "all" : mode === "none" ? [] : picked; + const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join(); @@ -6324,33 +6363,45 @@ diff --git a/client.js b/client.js + { className: "bt-secret-row-actions" }, + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) + ) -+ ); + ); } --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 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}`); +function ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); @@ -6361,18 +6412,12 @@ 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)); +- }) + }); - }; -- 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" }) ++ }; + const edit = (next) => { + setValue(next); + setReplaced(false); @@ -6390,8 +6435,14 @@ diff --git a/client.js b/client.js + value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ); } --function AdminStar() { -- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); +- +-// 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 SecretRow({ secret, roster, actions, run, open, onToggle }) { + const [confirm, setConfirm] = (0, import_react7.useState)(false); + (0, import_react7.useEffect)(() => { @@ -6435,11 +6486,13 @@ diff --git a/client.js b/client.js + ) : null + ); } --function MainStar() { -- return (0, import_react2.createElement)( +-function PeopleIcon() { +- return (0, import_react3.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" }) +- { 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 AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); @@ -6469,15 +6522,8 @@ diff --git a/client.js b/client.js + ) ); } --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 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 SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); @@ -6616,40 +6662,26 @@ diff --git a/client.js b/client.js + if (known) { + known.seats.push(seat); + continue; - } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; ++ } + 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 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 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 facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; } --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 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/held-reply.js +var import_react8 = require("react"); @@ -6659,21 +6691,7 @@ diff --git a/client.js b/client.js + constructor(props) { + super(props); + this.state = { failed: false }; - } -- 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 ++ } + static getDerivedStateFromError() { + return { failed: true }; + } @@ -6722,25 +6740,8 @@ diff --git a/client.js b/client.js + // different field elsewhere. A failure that can never succeed draws no button on any surface. + failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null, + error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null - ); - } -- const seats = clusterSeats(Math.min(people.length, 4), size); -- const overflow = people.length > 4 ? people.length - 3 : 0; -- const gap = size / 14; -- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-cluster", style: { width: size, height: size } }, -- seats.map(([x, y, s], index) => { -- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); -- const style = { left: x, top: y, width: s, height: s }; -- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); -- if (overflow > 0 && index === 3) { -- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); -- } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) ++ ); ++ } + const plain = (0, import_react8.createElement)( + "div", + { className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") }, @@ -6753,34 +6754,44 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) + ), + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null -+ ); + ); +-} +-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" }) + return (0, import_react8.createElement)( + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain ); } - --// src/client/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 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" })); ++ +// src/client/turn-reasoning.js +function turnReasoning(steps, current) { + const members = steps.length ? steps : [current]; + if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; + return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); } --function PeopleIcon() { +-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: 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" }) +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) +- ); +-}; +-var 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 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(); @@ -6832,8 +6843,32 @@ diff --git a/client.js b/client.js + }) ); } --function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); +-var 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 WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; +var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text2) { @@ -6848,13 +6883,30 @@ diff --git a/client.js b/client.js + } + return false; } --function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -- ); +-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; +function useDismiss(open, close, refs) { + (0, import_react9.useEffect)(() => { + if (!open) return void 0; @@ -6862,12 +6914,25 @@ diff --git a/client.js b/client.js + if (refs.some((ref) => ref.current?.contains(event.target))) return; + close(); + }; -+ const onKey = (event) => { + const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); + if (event.key === "Escape") { + event.stopPropagation(); + close(); -+ } -+ }; + } + }; +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); + return () => { @@ -6876,11 +6941,18 @@ diff --git a/client.js b/client.js + }; + }, [open]); } --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 QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); +- const options = optionsOf(question); +- const multi = question.multiSelect === true; +- const custom = own.trim(); +- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; +- const choose = (label) => { +- if (multi) { +- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react9.useState)(null); + const [place, setPlace] = (0, import_react9.useState)(null); @@ -6892,19 +6964,75 @@ diff --git a/client.js b/client.js + const open = (kind, event) => { + if (panel === kind) { + close(); -+ return; -+ } + return; + } +- onAnswer({ selected: [label] }); + const rect = event.currentTarget.getBoundingClientRect(); + const below = rect.bottom + 220 < window.innerHeight; + setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); + setPanel(kind); -+ }; + }; +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); + const copy2 = () => { + void navigator.clipboard?.writeText(text2).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1e3); + }); -+ }; + }; +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); +- } +- }) : null; +- const rows = options.map((option, optionIndex) => { +- const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( +- "button", +- { +- key: option.label, +- type: "button", +- className: "bt-q-opt", +- "aria-pressed": multi ? selected : void 0, +- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), +- onClick: () => choose(option.label) +- }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( +- "span", +- { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null +- ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( + const download2 = () => { + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); + const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); @@ -6922,7 +7050,25 @@ diff --git a/client.js b/client.js + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, + (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); +- } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( + { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + REACTIONS.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { + close(); @@ -6930,18 +7076,22 @@ diff --git a/client.js b/client.js + } }, emoji)) + ), document.body) : null, + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) + { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, + item(t("Copy"), CopyIcon, copy2), + item(t("Download"), DownloadIcon, download2) + ), document.body) : null - ); - } --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 PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -6974,12 +7124,24 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), + t("Thought it through"), + peek ? (0, import_react9.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_react9.createElement)("div", { className: "bt-think-text" }, text2) ); } --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 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 useFileMentions({ node, openFile, fileMentions, useTurnData }) { + const tail = useTurnData?.("turn-tail"); + const location2 = node.location; @@ -6989,55 +7151,13 @@ diff --git a/client.js b/client.js + 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]); - } --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); --var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) -- ); --}; --var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; --function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -- ); ++} +function useClosingText(node, useTurnData) { + const tail = useTurnData?.("turn-tail"); + const seq = node.data.finalNode?.seq; + if (!tail?.closing || seq === void 0) return true; + return tail.closing.finalNode?.seq === seq; - } --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 friendLead(bot, texts2) { + if (bot?.remote === void 0 || texts2.length === 0) return void 0; + if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; @@ -7080,48 +7200,8 @@ diff --git a/client.js b/client.js + }), + reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles - ); - } --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(); -- } -- }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); ++ ); ++} +function HeldReply({ held, roster, sessionId, actions, closing, children }) { + const bot = roster.byId[sessionId]; + const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to); @@ -7132,66 +7212,7 @@ diff --git a/client.js b/client.js + children, + closing ? (0, import_react9.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null + ); - } --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_react4.useState)(""); -- const [picked, setPicked] = (0, import_react4.useState)([]); -- const ownRef = (0, import_react4.useRef)(null); -- const titleId = (0, import_react4.useId)(); -- const options = optionsOf(question); -- const multi = question.multiSelect === true; -- const custom = own.trim(); -- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -- const choose = (label) => { -- if (multi) { -- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); -- 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(); -- } -- }) : 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 ++} +function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) { + const lead = (0, import_react9.createElement)("span", { className: "bt-lead", title: name }, (0, import_react9.createElement)(PersonMark, { name })); + return (0, import_react9.createElement)( @@ -7204,7 +7225,13 @@ diff --git a/client.js b/client.js +function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { + if (whole && bots.length === 1) { + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) + { className: "bt-event" }, + (0, import_react9.createElement)( + "button", @@ -7212,20 +7239,10 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)("span", null, prefix), + (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name) -+ ) + ) ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( +- return (0, import_react4.createElement)( + const parts = []; + parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix)); + bots.forEach((bot, index) => { @@ -7267,47 +7284,12 @@ diff --git a/client.js b/client.js + const held = heldLineState(room2, { seq, botId: bot.id, text: text2 }); + if (held === "rejected") { + return (0, import_react9.createElement)( - "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) ++ "div", + { className: "bt-event bt-not-sent", "data-flybot": "held-not-sent", role: "status" }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) - ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); - } -- return (0, import_react4.createElement)( -- "form", -- { -- className: raised ? "bt-q bt-q-raised" : "bt-q", -- "aria-labelledby": titleId, -- onSubmit: (event) => { -- event.preventDefault(); -- submit(); -- } -- }, -- (0, import_react4.createElement)( -- "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 ++ ); ++ } + if (held === "held" || isHeldLine(room2, text2, bot)) return null; + const lead = (0, import_react9.createElement)( + "button", @@ -7321,18 +7303,54 @@ diff --git a/client.js b/client.js + room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + ); + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) + { className: "bt-group-msg" }, + name, + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ); } --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 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); @@ -7373,14 +7391,7 @@ diff --git a/client.js b/client.js + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + if (person) { + return (0, import_react9.createElement)( - "div", -- { 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-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle }, + // One short header: the person, and the Bot that carried the line only when it is known (#275). + (0, import_react9.createElement)( @@ -7391,25 +7402,14 @@ diff --git a/client.js b/client.js + ), + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null - ); - } -- return (0, import_react4.createElement)( ++ ); ++ } + const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react9.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)) -- ); -- })) +- { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) + { className: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react9.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react9.createElement)( @@ -7422,59 +7422,18 @@ diff --git a/client.js b/client.js + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null ); } --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 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_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot }))); - } --function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); -- if (answered === question.id) return null; -- const onAnswer = (answer) => { -- setAnswered(question.id); -- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); -- }; -- const onDismiss = () => { -- setAnswered(question.id); -- void actions.dismissQuestion(sessionId); -- }; -- return (0, import_react4.createElement)( -- "div", -- { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) -+function GlobeMark() { -+ return (0, import_react9.createElement)( -+ "svg", -+ { width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, -+ (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }), -+ (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) - ); - } -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_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.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 : []; @@ -7487,7 +7446,14 @@ diff --git a/client.js b/client.js - question, - answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } - }))); --} ++function GlobeMark() { ++ return (0, import_react9.createElement)( ++ "svg", ++ { width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, ++ (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }), ++ (0, import_react9.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"); @@ -7561,11 +7527,22 @@ diff --git a/client.js b/client.js + setBusy(""); } - }); --} ++ }; ++ return (0, import_react9.createElement)( ++ "div", ++ { className: "bt-file-chips" }, ++ files.map((file) => (0, import_react9.createElement)( ++ "button", ++ { key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) }, ++ `📎 ${file.name} · ${fileSize(file.size)}` ++ )), ++ error ? (0, import_react9.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_react9.createElement)( "div", - { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, @@ -7578,18 +7555,21 @@ diff --git a/client.js b/client.js - className: "bt-secret-seg", - onClick: () => onChange(id) - }, text)) -+ { className: "bt-file-chips" }, -+ files.map((file) => (0, import_react9.createElement)( -+ "button", -+ { key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) }, -+ `📎 ${file.name} · ${fileSize(file.size)}` -+ )), -+ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null ++ { className: "bt-event" }, ++ (0, import_react9.createElement)("span", null, prefix), ++ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null ); } -function SecretHead({ bot, title, detail, name, onClose }) { - return (0, import_react5.createElement)( -+function GroupEvent({ prefix, room: room2, name, roster, actions }) { ++function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { ++ if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; ++ const self = roster.byId[sessionId]; ++ const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); ++ const other = team || !scope ? void 0 : findBot(scope); ++ const target = other && other.id !== self?.id ? other : self; ++ if (!target) return null; ++ const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); + return (0, import_react9.createElement)( "div", - { className: "bt-q-head bt-secret-head" }, @@ -7602,21 +7582,6 @@ diff --git a/client.js b/client.js - ), - onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + { className: "bt-event" }, -+ (0, import_react9.createElement)("span", null, prefix), -+ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null -+ ); -+} -+function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -+ if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -+ const self = roster.byId[sessionId]; -+ const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -+ const other = team || !scope ? void 0 : findBot(scope); -+ const target = other && other.id !== self?.id ? other : self; -+ if (!target) return null; -+ const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); -+ return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-event" }, + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) ); } @@ -7788,7 +7753,6 @@ diff --git a/client.js b/client.js - )), - 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; @@ -7868,20 +7832,10 @@ diff --git a/client.js b/client.js + if (source.kind === "user-question-reply") return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); + } - } --function RequestView({ bot, request, roster, actions }) { -- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); ++} +function NewDivider() { + return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.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_react9.useEffect)(() => { @@ -7901,18 +7855,8 @@ diff --git a/client.js b/client.js + return fresh ? (0, import_react9.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 }))); ++ 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([ @@ -8005,38 +7949,7 @@ diff --git a/client.js b/client.js + if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true; + } + return false; - } --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 settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; +function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { + const turn = location2.turn; @@ -8062,83 +7975,21 @@ diff --git a/client.js b/client.js + fresh ? (0, import_react9.createElement)(NewDivider) : null ); } --function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!long) return; -- void Promise.resolve(onSave(value)).then((ok) => { -- if (ok) { -- setValue(""); -- setReplaced(true); -- } -- }); -- }; -- const edit = (next) => { -- setValue(next); -- setReplaced(false); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -- ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null -- ); +-function RequestView({ bot, request, roster, actions }) { +- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); +function Shimmer({ children }) { + return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children); } --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 -- ); -+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"] +-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") }))); ++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"], @@ -8192,135 +8043,32 @@ diff --git a/client.js b/client.js + 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) }; -+ } + } +- return 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 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 workPhrase(tool) { + if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); } --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"))) -- ]; +-function ScopeEditor({ secret, roster, onSave }) { +- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); +- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); +- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( +function workText(tool, step) { + return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; - } --var SECRET_CSS = ` --.bt-secret-head{align-items:center;gap:10px} --.bt-secret-who{position:relative;flex:none;display:inline-flex} --.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} --.bt-secret-lock svg{width:11px;height:11px} --.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} --.bt-secret-body{display:flex;flex-direction:column;gap:8px} --.bt-secret-field{align-items:center;min-height:36px} --.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} --.bt-secret-input[data-masked]{-webkit-text-security:disc} --.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} --.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} --.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-secret-glide[data-at="1"]{transform:translateX(100%)} --.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} --.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} --.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --/* A key card asks the user to act, so the process group carrying it stays open in every -- work-details mode (same override as .bt-group-replies in styles.js). */ --[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} --[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} --.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} --.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} --@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} --.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} --.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} --.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} --.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} --.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} --.bt-secret-done .bt-q-head{align-items:center} --.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} --.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} --.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} --@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} --@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} --.bt-secret-event{display:inline-flex;align-items:center;gap:5px} --.bt-secret-dock{flex-direction:column;align-items:center} --.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} --.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} --.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} --.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} --.bt-secret-faces{display:inline-flex} --.bt-secret-faces>*+*{margin-left:-6px} --.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} --.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} --.bt-secret-radios{display:flex;gap:16px;font-size:13px} --.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} --.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} --.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} --.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} --.bt-secret-bot svg{width:13px;height:13px} --.bt-secret-inline{display:flex;gap:8px;align-items:center} --.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} --.bt-secret-row-actions .bt-set-hint{flex:1} --.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} --.bt-secret-danger svg{width:13px;height:13px} --.bt-secret-add .bt-input{height:34px} --@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} --`; -- --// src/client/turn-reasoning.js --function turnReasoning(steps, current) { -- const members = steps.length ? steps : [current]; -- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); ++} +function useLiveNote(actions, sessionId, id, entry) { + const key = entry ? JSON.stringify(entry) : ""; + (0, import_react9.useEffect)(() => { @@ -8328,26 +8076,7 @@ diff --git a/client.js b/client.js + actions.noteLive(sessionId, id, JSON.parse(key)); + return () => actions.noteLive(sessionId, id, null); + }, [sessionId, id, key]); - } --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 GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) { + const [live, setLive] = (0, import_react9.useState)({ replies: [] }); + (0, import_react9.useEffect)(() => { @@ -8384,16 +8113,30 @@ diff --git a/client.js b/client.js + const room2 = roster.roomsById[sessionId]; + return (0, import_react9.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)( +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( +- "label", +- { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text +- )) +- ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { +- key: bot.id, +- type: "button", +- className: "bt-secret-bot", +- "aria-pressed": picked.includes(bot.id), +- onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) +- ) + { className: "bt-group-replies" }, + replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; @@ -8405,29 +8148,14 @@ diff --git a/client.js b/client.js + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( - "div", -- { className: "bt-msg-line" }, -- lead && last ? lead : null, -- (0, import_react6.createElement)( ++ "div", + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( - "div", -- { className: "bt-bubble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) ++ "div", + { className: "bt-ububble" }, + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" }) - ), -- 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 -- ); ++ ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); + } @@ -8488,16 +8216,19 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react9.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); - }) ++ }) ); } --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(); +-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); +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) { @@ -8581,24 +8312,84 @@ diff --git a/client.js b/client.js + parsed = JSON.parse(contentText(call4.content)); + } catch { + parsed = null; -+ } -+ const answer = parsed?.answers?.[0]; -+ const question = argValue("questions")?.[0]; -+ if (!answer) return null; -+ return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); -+ } -+ case "read_group_chat": { -+ if (!done) return null; -+ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -+ return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -+ } + } +- }); +- }; +- 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 +- ); ++ const answer = parsed?.answers?.[0]; ++ const question = argValue("questions")?.[0]; ++ if (!answer) return null; ++ return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); ++ } ++ case "read_group_chat": { ++ if (!done) return null; ++ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); ++ return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; ++ } + case "remember": + case "forget": + return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null; + default: + return null; + } -+} + } +-function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) +- ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null +- ); +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { + const roster = useRoster((value) => value); + const root = node.data.root; @@ -8614,7 +8405,35 @@ diff --git a/client.js b/client.js + if (drawn.length === 0) return null; + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); -+} + } +-function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); +- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!ready) return; +- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { +- if (ok) { +- setValue(""); +- onDone(); +- } +- }); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-set-row bt-set-paste bt-secret-add" }, +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) +- ) +- ); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -8622,7 +8441,30 @@ diff --git a/client.js b/client.js + const at = roster?.remoteAwaiting?.[sessionId]; + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; -+} + } +-function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; + +// src/client/remote-approval-wait.js +var REMOTE_APPROVAL_LABEL = "Waiting for their approval"; @@ -8630,8 +8472,76 @@ diff --git a/client.js b/client.js + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; -+} -+ + } +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +-`; + +-// src/client/turn-reasoning.js +-function turnReasoning(steps, current) { +- const members = steps.length ? steps : [current]; +- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; +- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); +// src/client/activity-row-host.js +var RUNNING_HOST = "[data-chat-flow] [data-chat-running]"; +var TAIL_HOST = "tail"; @@ -8640,8 +8550,57 @@ diff --git a/client.js b/client.js + if (typeof root.matches === "function" && root.matches("[data-chat-running]")) return root; + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); -+} -+ + } +-var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); +-var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); +-var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); + +-// src/client/cells.js +-var import_react6 = require("react"); +-var import_react_dom = require("react-dom"); +-var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); +-var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); +-function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { +- const onClick = (event) => { +- const target = mentionTarget(event); +- if (target === null) return; +- event.preventDefault(); +- event.stopPropagation(); +- actions.openSession(target); +- }; +- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) +- ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); +- }) +- ); +// src/client/activity-row.js +var import_react10 = require("react"); +var import_react_dom2 = require("react-dom"); @@ -8734,14 +8693,17 @@ diff --git a/client.js b/client.js + return () => { + cancelAnimationFrame(frame); + observer.disconnect(); - }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); ++ }; + }, [active, where]); + return host; -+} + } +-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(); +function useRowEntrances(replacing) { + const check = (0, import_react10.useRef)(replacing); + check.current = replacing; @@ -8754,7 +8716,11 @@ diff --git a/client.js b/client.js + if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; + delete event.currentTarget.dataset.btEnter; + event.currentTarget.removeEventListener("animationend", settle); -+ }; + }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); + const scan = () => { + frame = 0; + const next = document.querySelector("[data-chat-flow]"); @@ -9043,31 +9009,7 @@ diff --git a/client.js b/client.js + if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); + }, []); + return paneLeft; - } --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) -- ) -- ); -- } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); ++} + +// src/client/member-status.js +var import_react12 = require("react"); @@ -9094,7 +9036,31 @@ diff --git a/client.js b/client.js +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] })); -+} + } +-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 +- )); +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); @@ -9167,6 +9133,11 @@ diff --git a/client.js b/client.js } -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 MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -9200,7 +9171,16 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); -+} + } +-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)( +function useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -9241,8 +9221,7 @@ diff --git a/client.js b/client.js + return (0, import_react12.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 +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) + { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react12.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react12.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, @@ -9261,15 +9240,12 @@ diff --git a/client.js b/client.js + ) ); } --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)( +-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; +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -9279,38 +9255,111 @@ diff --git a/client.js b/client.js + (0, import_react12.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_react12.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); -+ } + } +- 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 })); +- }); + 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)); -+ }; + }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); +- return () => { +- alive.current = false; +- }; +- }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { +- const pending = restore.current; +- restore.current = null; +- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; +- }, [view.items]); +- (0, import_react6.useEffect)(() => { +- const node = top.current; +- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; +- const observer = new IntersectionObserver((entries2) => { +- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); +- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); +- observer.observe(node); +- return () => observer.disconnect(); +- }, [view.cursor, view.loading, view.failed]); +- const selfId = roster.byId[sessionId]?.id; +- const rows = []; +- for (const item of view.items) { +- const last = rows.at(-1); +- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); +- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); +- } +- return (0, import_react6.createElement)( + return (0, import_react12.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) +- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( + { className: "bt-mw-member" }, + (0, import_react12.createElement)( -+ "div", + "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; + { className: "bt-mw-state bt-mw-work" }, + (0, import_react12.createElement)("span", null, (0, import_react12.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) - ); - } --function 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 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 scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -- } -- return null; ++} +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} @@ -9356,26 +9405,7 @@ diff --git a/client.js b/client.js + accepted: "done", + cancelled: "done", + failed: "done" - }; --function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); -- const load = (cursor) => { -- if (busy.current) return; -- busy.current = true; -- setView((current) => ({ ...current, loading: true, failed: false })); -- actions.earlier(sessionId, cursor).then((page) => { -- busy.current = false; -- if (!alive.current) return; -- const scroller = top.current && scrollParent(top.current); -- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -- }, () => { -- busy.current = false; -- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); ++}; +function columnOf(task) { + return COLUMN_OF[task?.status] ?? "not-started"; +} @@ -9469,74 +9499,13 @@ diff --git a/client.js b/client.js + }).catch(() => { + }).finally(() => { + if (!stopped) timer = setTimeout(load, POLL_MS2); - }); -- }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); ++ }); + load(); - return () => { -- alive.current = false; ++ return () => { + stopped = true; + clearTimeout(timer); + taskListeners.delete(setTasks); - }; -- }, [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; ++ }; + }, []); + return tasks; +} @@ -9849,48 +9818,7 @@ diff --git a/client.js b/client.js + reason, + ...caller === void 0 ? {} : { bot: caller } + }); - } --} --function NewDivider() { -- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); --} --function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { -- if (!open) turnProcess.setOpen(true); -- }, [open, turnProcess]); -- const turn = node?.location?.turn?.turn ?? node?.data?.turn; -- const start = node?.location?.turn?.start?.time; -- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "first", start); -- }, [sessionId, turn, start]); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- const own = state[sessionId]?.[turn]; -- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; -- }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; --} --function Nothing() { -- return null; --} --var SEPARATOR_GAP_MS = 30 * 60 * 1e3; --function clockLabel(time, now = Date.now()) { -- const date = new Date(time); -- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -- if (days === 0) return t("Today {time}", { time: clock4 }); -- if (days === 1) return t("Yesterday {time}", { time: clock4 }); -- const sameYear = date.getFullYear() === new Date(now).getFullYear(); -- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; --} --function 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; ++ } + if (sessions?.byId) { + for (const bot of roster.bots ?? []) { + if (ownBot(roster, bot.id) === void 0 || work.bots?.[bot.id] !== void 0 || waitingAt.has(`${bot.id}|`)) continue; @@ -9902,17 +9830,7 @@ diff --git a/client.js b/client.js + if (text2 === "") continue; + done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text2, place: "", chat: bot.id, ...people(roster, bot.id) }); + } - } -- return next === void 0 ? void 0 : turns[next].first; --} --function openingTurn(turns, firstTurns, hiddenTurns) { -- if (!turns || !firstTurns) return null; -- const start = nextTurnStart(turns, -Infinity); -- if (start === void 0) return null; -- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -- const listed = firstTurns.split(",").map(Number); -- const earlier = listed.filter((entry) => entry < turn); -- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; ++ } + const newest = (a, b) => (b.at ?? Infinity) - (a.at ?? Infinity); + for (const card of [...needs, ...progress, ...done]) { + card.activity = latestActivity(card, { roster, work, sessions }); @@ -9947,35 +9865,14 @@ diff --git a/client.js b/client.js + const note = choice !== null && typeof choice === "object" ? String(choice.note ?? "").trim() : ""; + if (note === "") return void 0; + return Promise.resolve(actions.sendBackTask(card.task.id, note)); -+ } + } + if (card?.kind === "approval" && card.approval?.answerable === true) { + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 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 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 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"))); +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -10028,44 +9925,25 @@ diff --git a/client.js b/client.js + redraw((count) => count + 1); + } }, t("Send back")), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "accept") }, t("Accept")) - ); ++ ); } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); --} --var PHRASES = { -- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] --}; --var TOOL_PHRASES = [ -- [/web_?search|search_?web/i, "Searching online"], -- [/fetch|browse|browser|web/i, "Reading a page"], -- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], -- [/^(read|view|cat)\b|read_/i, "Opening a file"], -- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], -- [/write|edit|patch|apply|replace/i, "Writing changes"], -- [/image|photo|draw/i, "Working on an image"] --]; --var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; --function toolActivity(name, arg, findBot, self) { -- switch (name) { -- case "ask_user": -- return null; -- case "request_secret": -- return null; -- case "list_secrets": -- return { label: t("Checking the keys"), state: "searching" }; -- case "ask_user_question": -- return { label: t("Waiting for your answer"), state: "alert" }; -- case "group_relay": -- return { relay: true, state: "orbit" }; -- case "message_bot": { -- const target = findBot(arg("to")); -- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -- } -- case "stop_bot": { -- const target = findBot(arg("bot")); -- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +-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 AnswerFields({ card, busy, onAnswer }) { + const [, redraw] = (0, import_react13.useState)(0); + const question = card.kind === "question"; @@ -10082,12 +9960,7 @@ diff --git a/client.js b/client.js + if (selected.length === 0 && custom === "") return; + onAnswer(card, { selected, custom }); + return; - } -- 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" }; ++ } + if (custom === "") return; + onAnswer(card, custom); + }; @@ -10096,23 +9969,7 @@ diff --git a/client.js b/client.js + if (!multi) { + onAnswer(card, { selected: [label], custom: "" }); + return; - } -- 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) }; ++ } + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -10146,7 +10003,9 @@ diff --git a/client.js b/client.js + }), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); -+} + } +-function Nothing() { +- return null; +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { + const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; @@ -10170,15 +10029,31 @@ diff --git a/client.js b/client.js + wide ? (0, import_react13.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null + ); -+} + } +-var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +-function clockLabel(time, now = Date.now()) { +- const date = new Date(time); +- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); +- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); +- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); +- if (days === 0) return t("Today {time}", { time: clock4 }); +- if (days === 1) return t("Yesterday {time}", { time: clock4 }); +- const sameYear = date.getFullYear() === new Date(now).getFullYear(); +- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +function Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; + return (0, import_react13.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", null, t(label))); -+} + } +-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 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 next === void 0 ? void 0 : turns[next].first; + return typeof card.at === "number" ? relativeShort(card.at) : ""; +} +function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) { @@ -10241,9 +10116,14 @@ diff --git a/client.js b/client.js + ) + ); } --function workPhrase(tool) { -- if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +-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 useNow2(ms) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -10252,51 +10132,37 @@ diff --git a/client.js b/client.js + }, [ms]); + return now; } --function useLiveNote(actions, sessionId, id, entry) { -- const key = entry ? JSON.stringify(entry) : ""; +-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)(() => { -- if (!key) return void 0; -- actions.noteLive(sessionId, id, JSON.parse(key)); -- return () => actions.noteLive(sessionId, id, null); -- }, [sessionId, id, key]); -+function openCard(actions, card) { -+ actions.closeInbox(); -+ if (card?.kind === "workflow") { -+ actions.openSettings("workflow"); -+ return; -+ } -+ actions.openSession(card.chat); - } --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { -- if (done) return void 0; -- let stop = false; -- let timer; -- let seen = ""; -- const tick = async () => { -- const result = await actions.roomProgress(sessionId); -- if (stop) return; -- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; -- if (entry && key !== seen) { -- seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); -- } -- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); -- }; -- void tick(); -- return () => { -- stop = true; -- clearTimeout(timer); -- }; -- }, [done, sessionId, callId]); -- let replies = live.replies; -- if (done) { -- try { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; +- 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 openCard(actions, card) { ++ actions.closeInbox(); ++ if (card?.kind === "workflow") { ++ actions.openSettings("workflow"); ++ return; ++ } ++ actions.openSession(card.chat); ++} +function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { + const work = useWork((value) => value); + const sessions = useSessions((value) => value); @@ -10323,13 +10189,7 @@ diff --git a/client.js b/client.js + if (pending === void 0) { + setBusy(""); + return; - } -- } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; -- const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ } + void Promise.resolve(pending).then(() => { + setAnswered((value) => ({ ...value, [card.id]: true })); + const name = card.bot?.name ?? ""; @@ -10338,73 +10198,14 @@ diff --git a/client.js b/client.js + }; + const open = (card) => openCard(actions, card); + 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", + { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 }, + (0, import_react13.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react13.createElement)( + "div", + { className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" }, + (0, import_react13.createElement)( - "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); -- }) ++ "div", + { className: "bt-mem-head" }, + (0, import_react13.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")), + (0, import_react13.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")), @@ -10439,17 +10240,8 @@ diff --git a/client.js b/client.js + ) ); } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; -- const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +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)} @@ -10513,18 +10305,11 @@ diff --git a/client.js b/client.js + key, + id: String(task.id), + title: String(task.title ?? "") - }; -- visit(root); -- return calls; ++ }; } --function useShownCards(actions, sessionId, callIds) { -- const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { -- const ids = JSON.parse(key); -- if (ids.length === 0 || !actions.noteCards) return void 0; -- actions.noteCards(sessionId, ids, 1); -- return () => actions.noteCards(sessionId, ids, -1); -- }, [sessionId, key]); +-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"] + +// src/client/board-card.js +var import_react14 = require("react"); @@ -10538,7 +10323,42 @@ diff --git a/client.js b/client.js + label: "bt-board-label", + owner: "bt-board-who", + note: "bt-board-note" -+}; + }; +-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 BoardCard({ + card, + skin = TASK_SKIN, @@ -10587,51 +10407,7 @@ diff --git a/client.js b/client.js + footer, + actions + ); - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); ++} + +// src/client/kanban.js +var import_react15 = require("react"); @@ -10711,10 +10487,18 @@ diff --git a/client.js b/client.js + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- 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" }; + }; + 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 })); @@ -10748,10 +10532,42 @@ diff --git a/client.js b/client.js + return t("Cancelled"); + case "failed": + return t("Failed"); -+ default: + 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) }; + return t("Not started"); -+ } -+} + } + } +-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]); +-} +-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); +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -10773,16 +10589,7 @@ diff --git a/client.js b/client.js + if (!map.has(id)) { + map.set(id, []); + order.push(id); - } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; ++ } + map.get(id).push(task); + } + return order.map((id) => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })); @@ -10809,13 +10616,19 @@ diff --git a/client.js b/client.js + if (stopped || !Array.isArray(list)) return; + setArchive(list.filter((task) => columnOf(task) === "done")); + }).catch(() => void 0); -+ }; + }; +- void tick(); + load(); + const timer = setInterval(load, POLL_MS3); -+ return () => { + return () => { +- stop = true; +- clearTimeout(timer); + stopped = true; + clearInterval(timer); -+ }; + }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { + }, [key]); + const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + const replace = (task) => { @@ -10919,11 +10732,7 @@ diff --git a/client.js b/client.js + const id = event.dataTransfer?.getData("text/plain"); + if (id) onMove?.(id, column); + onDrag?.(null); - } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); ++ } + }, + (0, import_react15.createElement)( + "h3", @@ -11062,15 +10871,20 @@ diff --git a/client.js b/client.js + const task = tasks.find((item) => item.id === id) ?? own.find((item) => item.id === id); + if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return; + setError(""); -+ try { + try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; + replace(await actions.taskUpdate({ id, column })); + } catch (failure) { + setError(failure?.message ?? String(failure)); } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +- } +- const 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)( + }; + const create = async (title) => { + const text2 = String(title ?? "").trim(); @@ -11081,24 +10895,77 @@ diff --git a/client.js b/client.js + setDraft(""); + } catch (failure) { + setError(failure?.message ?? String(failure)); - } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; -- } ++ } + }; + const name = scopeName(roster, scope); + return (0, import_react15.createElement)( -+ "div", + "div", +- { className: "bt-group-replies" }, +- replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); +- } +- if (reply.kind === "failed") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, +- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) +- ); +- } +- if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) +- ); +- } +- if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); +- } +- if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); +- } +- if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); +- } +- if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) +- ) +- ); +- } +- if (reply.kind === "stopped") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); +- } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); +- }), +- pending.map((id) => { +- const bot = roster.byId[id]; +- if (!bot) return null; +- return (0, import_react6.createElement)( + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react15.createElement)( + "div", + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react15.createElement)( -+ "div", + "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); +- }) + { className: "bt-mem-head" }, + (0, import_react15.createElement)("h2", { id: "bt-board-heading" }, t("Board")), + (0, import_react15.createElement)("span", { className: "bt-mem-status" }, name), @@ -11123,23 +10990,22 @@ diff --git a/client.js b/client.js + onCreate: create + }) + ) -+ ); + ); } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { -- const roster = useRoster((value) => value); -- const root = node.data.root; -- const turn = node.location?.turn?.turn ?? root.turn; -- const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); -- const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); -- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); -- if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +-var 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; +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)} @@ -11202,73 +11068,122 @@ diff --git a/client.js b/client.js + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} } +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +`; - --// src/client/activity-row.js --var import_react7 = require("react"); --var import_react_dom2 = require("react-dom"); --var PHRASE_HOLD_MS = 800; --var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); --function elapsedLabel(ms) { -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${Math.max(1, minutes)}m`; -- const hours = Math.floor(minutes / 60); -- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++ +// src/client/role-labels.js +var ROLE_LABELS = { owner: "Owner", maintainer: "Maintainer", member: "Member" }; +function roleText(role) { + if (role === null || role === void 0 || role === "") return ""; + return t(ROLE_LABELS[role] ?? String(role)); } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { -- if (!active) { -- setHost(null); -- return void 0; +-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 }); - } -- let frame = 0; -- const find = () => { -- frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -- setHost((prev) => prev === node ? prev : node); -- }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(find); -- }); -- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -- find(); -- return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- }; -- }, [active]); -- return host; -+ -+// src/client/projects-nav.js -+var import_react16 = require("react"); -+var import_react_dom3 = require("react-dom"); -+var PROJECTS_NAV_CSS = ` -+.bt-projects-row { display: flex; align-items: center; gap: 8px; } -+.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; } -+.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); } -+.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; } -+.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; } -+.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; } -+.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); } -+.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -+.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; } -+.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; } -+.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); } -+.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; } -+.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; } -+.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; } -+.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; } -+.bt-project-menu-item[disabled] { opacity: .6; cursor: default; } -+`; -+function projectHash(id) { -+ return `#project=${encodeURIComponent(String(id ?? ""))}`; -+} +- 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; ++ ++// src/client/projects-nav.js ++var import_react16 = require("react"); ++var import_react_dom3 = require("react-dom"); ++var PROJECTS_NAV_CSS = ` ++.bt-projects-row { display: flex; align-items: center; gap: 8px; } ++.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; } ++.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); } ++.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; } ++.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; } ++.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; } ++.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); } ++.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } ++.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; } ++.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; } ++.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); } ++.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; } ++.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; } ++.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; } ++.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; } ++.bt-project-menu-item[disabled] { opacity: .6; cursor: default; } ++`; ++function projectHash(id) { ++ return `#project=${encodeURIComponent(String(id ?? ""))}`; ++} +function projectName(project) { + const name = typeof project?.name === "string" ? project.name.trim() : ""; + return name === "" ? `#${project?.id ?? ""}` : name; @@ -11280,49 +11195,40 @@ 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 }); -+ } + } + return items; } --function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); -- check.current = replacing; -- (0, import_react7.useEffect)(() => { -- const seen = /* @__PURE__ */ new WeakSet(); -- const started = Date.now(); -- let column = null; -- let frame = 0; -- const settle = (event) => { -- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -- delete event.currentTarget.dataset.btEnter; -- event.currentTarget.removeEventListener("animationend", settle); -- }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; -- }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); +-function 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 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 }); -+} + } +- +-// 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`; +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)( + "button", @@ -11337,7 +11243,7 @@ diff --git a/client.js b/client.js + onContextMenu: (event) => { + event.preventDefault(); + onRole?.(project, event); - } ++ } + }, + (0, import_react16.createElement)("span", { className: "bt-project-entry-name" }, projectName(project)), + roleText(project.role) === "" ? null : (0, import_react16.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role)) @@ -11348,14 +11254,50 @@ diff --git a/client.js b/client.js + rows, + note === "" || note === void 0 ? null : (0, import_react16.createElement)("div", { className: "bt-projects-note", role: "status" }, note) + ); -+} + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +- } +- let frame = 0; +- const find = () => { +- frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); +- setHost((prev) => prev === node ? prev : node); +- }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(find); +- }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); +- return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); +- }; +- }, [active]); +- return host; +function Chevron() { + return (0, import_react16.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_react16.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }) + ); -+} + } +-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); +function ProjectsMark() { + return (0, import_react16.createElement)( + "svg", @@ -11370,13 +11312,36 @@ diff --git a/client.js b/client.js + const onDown = (event) => { + if (!ref.current?.contains(event.target)) onClose(); }; +- 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") onClose(); + }; - const observer = new MutationObserver(() => { - if (!frame) frame = requestAnimationFrame(scan); - }); - scan(); -+ const onKey = (event) => { -+ if (event.key === "Escape") onClose(); -+ }; + window.addEventListener("mousedown", onDown, true); + window.addEventListener("keydown", onKey, true); return () => { @@ -11415,79 +11380,10 @@ diff --git a/client.js b/client.js - if (wait <= 0) { - make(); - return void 0; -- } -- const timer = setTimeout(make, wait); -- return () => clearTimeout(timer); -- }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); -- leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { -- if (visible) { -- setPhase("in"); -- return void 0; -- } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); -- 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)( + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react16.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); ++ "div", + { + ref, + className: "bt-project-menu", @@ -11517,7 +11413,19 @@ diff --git a/client.js b/client.js + if (onOpen) { + onOpen(id); + return; -+ } + } +- const timer = setTimeout(make, wait); +- return () => clearTimeout(timer); +- }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); +- leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { +- if (visible) { +- setPhase("in"); +- return void 0; + if (typeof window !== "undefined") window.location.hash = projectHash(id); + }, [onOpen]); + const [expanded, setExpanded] = (0, import_react16.useState)(false); @@ -11534,7 +11442,39 @@ diff --git a/client.js b/client.js + } catch (failure) { + setProjects([]); + setError(failure?.message ?? String(failure)); -+ } + } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); +- return () => clearTimeout(timer); +- }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { +- if (phase !== null) return; +- endedAt.current = Date.now(); +- setShown(null); +- const before = shift.current; +- shift.current = null; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (before === null || !scroller || reducedMotion()) return; +- const delta = before - scroller.scrollTop; +- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); +- }, [phase]); +- (0, import_react7.useEffect)(() => { +- if (!running) actions.clearLive(sessionId); +- }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { +- if (phase !== "in") return void 0; +- const timer = setInterval(() => setNow(Date.now()), 1e4); +- return () => clearInterval(timer); +- }, [phase]); +- const host = useRunningHost(running); +- if (!host || phase === null || !shown) return null; +- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( + }, [read]); + (0, import_react16.useEffect)(() => { + if (!expanded) return; @@ -11550,7 +11490,30 @@ diff --git a/client.js b/client.js + }; + const listed = expanded && wide; + return (0, import_react16.createElement)( -+ "div", + "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); + { className: "bt-projects" }, + (0, import_react16.createElement)( + "button", @@ -12004,7 +11967,7 @@ diff --git a/client.js b/client.js useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); useLandOnMainBot(roster, useSessions, actions); const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -@@ -5045,39 +5045,43 @@ function TeamSidebar(props) { +@@ -5045,42 +5045,258 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; @@ -12065,168 +12028,156 @@ diff --git a/client.js b/client.js + const [dshError, setDshError] = (0, import_react17.useState)(""); + const button = (0, import_react17.useRef)(null); + (0, import_react17.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; -@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) { - setDshError(""); - void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); - }; -- 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 }); ++ 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); ++ }; ++ }, [open]); ++ const phase = update?.phase ?? "idle"; ++ const dsh = update?.dsh; ++ const dshPhase = dsh?.phase ?? "idle"; ++ const dshRow = showsDshRow(dsh, update?.app); ++ if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; ++ const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; ++ const toggle = () => { ++ const rect = button.current?.getBoundingClientRect(); ++ if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); ++ setOpen((value) => !value); ++ }; ++ const start = () => { ++ setError(""); ++ void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); ++ }; ++ const startDsh = () => { ++ setDshError(""); ++ void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); ++ }; + const label = phase === "installing" ? t("Updating FlyBot core…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("FlyBot core {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("FlyBot core {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, ++ 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 FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); + return (0, import_react17.createElement)( + import_react17.Fragment, - null, -- (0, import_react8.createElement)("button", { ++ null, + (0, import_react17.createElement)("button", { - ref: button, - type: "button", - className: "bt-round bt-update", -@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) { - "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)( ++ 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_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.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, ++ "div", + { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box }, + update?.available || phase !== "idle" ? (0, import_react17.createElement)( + import_react17.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)( ++ null, + (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), + (0, import_react17.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_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react17.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", { ++ "div", ++ { className: "bt-update-actions" }, + update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react17.createElement)("button", { - type: "button", - className: "bt-update-go", - disabled: phase === "installing", -@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) { - }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) - ) - ) : null, -- dshRow ? (0, import_react8.createElement)( ++ 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_react17.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-dsh" }, + (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText), + dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, + (0, import_react17.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", { ++ "div", ++ { className: "bt-update-actions" }, + dsh.kind === "app" ? dsh.available ? (0, import_react17.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_react17.createElement)("button", { - type: "button", - className: "bt-update-go", - disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { - ), document.body) : null - ); - } --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), []); ++ 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 ++ ); ++} +function ConnectApps({ wide, actions }) { - 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); -- }; -- return (0, import_react8.createElement)( ++ if (!wide) return null; + return (0, import_react17.createElement)( - "span", - { className: "bt-connect-wrap" }, -- (0, import_react8.createElement)( ++ "span", ++ { className: "bt-connect-wrap" }, + (0, import_react17.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") -- }, -- (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)( -- "span", -- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, -- t("Plugin compatibility is not fully verified.") -- ), document.body) : null ++ "button", + { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, + (0, import_react17.createElement)(PlugIcon), + t("Connect apps") + ) - ); - } - function YouAvatar() { -- return (0, import_react8.createElement)( ++ ); ++} ++function YouAvatar() { + return (0, import_react17.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-you-wrap" }, + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) - ); - } - var openShellSettings = null; -@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { - else openHarnessSettings(); - }); - } -+var MODELS_STEP_ID = "flybot-models"; -+var MODELS_STEP_ORDER = 1e3; -+function ModelsSettingsStep({ explicit, complete, openSection }) { -+ (0, import_react17.useLayoutEffect)(() => { -+ if (explicit) openSection("models"); ++ ); ++} ++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(); ++ }); ++} ++var MODELS_STEP_ID = "flybot-models"; ++var MODELS_STEP_ORDER = 1e3; ++function ModelsSettingsStep({ explicit, complete, openSection }) { ++ (0, import_react17.useLayoutEffect)(() => { ++ if (explicit) openSection("models"); + complete(); + }, []); + return null; @@ -12234,179 +12185,285 @@ diff --git a/client.js b/client.js +function openHarnessModels() { + openHarnessOnboarding(MODELS_STEP_ID); +} - function openThroughMenu(trigger) { - const before = new Set(document.querySelectorAll('[role="menu"]')); - trigger.click(); -@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() { - })); - } - function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { -- const [open, setOpen] = (0, import_react8.useState)(false); -- (0, import_react8.useLayoutEffect)(() => { ++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 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 AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { + const [open, setOpen] = (0, import_react17.useState)(false); + (0, import_react17.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 (!seat) return void 0; ++ actions.seatAccount(true); ++ return () => actions.seatAccount(false); ++ }, [seat]); + const [box, setBox] = (0, import_react17.useState)(null); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - if (settingsOpen) setOpen(false); - }, [settingsOpen]); -- (0, import_react8.useEffect)(() => { ++ if (settingsOpen) setOpen(false); ++ }, [settingsOpen]); + (0, import_react17.useEffect)(() => { - if (!openSettings) return void 0; - openShellSettings = openSettings; - return () => { - if (openShellSettings === openSettings) openShellSettings = null; - }; - }, [openSettings]); -- (0, import_react8.useEffect)(() => { ++ if (!openSettings) return void 0; ++ openShellSettings = openSettings; ++ return () => { ++ if (openShellSettings === openSettings) openShellSettings = null; ++ }; ++ }, [openSettings]); + (0, import_react17.useEffect)(() => { - if (!openOnboarding) return void 0; - openShellOnboarding = openOnboarding; - return () => { - if (openShellOnboarding === openOnboarding) openShellOnboarding = null; - }; - }, [openOnboarding]); -- (0, import_react8.useEffect)(() => { ++ if (!openOnboarding) return void 0; ++ openShellOnboarding = openOnboarding; ++ return () => { ++ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; ++ }; ++ }, [openOnboarding]); + (0, import_react17.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; -@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi - 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")] +- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; ++ if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; + setOpen(false); + }; + const onKey = (event) => { +@@ -5097,412 +5097,1340 @@ function UpdateButton({ useUpdate, actions }) { + 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; + 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 }); ++ if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); + 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 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, ++ const groups = [ ++ [ ++ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], + [t("Connect apps"), ConnectorIcon, () => actions.openConnectors()], + [t("Plugins"), PlugIcon, () => actions.selectPanel("plugins")], + [t("Scheduled tasks"), ClockIcon, () => actions.openSettings("schedules")], + [t("Settings"), GearIcon, () => actions.openSettings("general")] - ], - [ - [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] - ] - ]; -- return (0, import_react8.createElement)( -- import_react8.Fragment, ++ ], ++ [ ++ [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] ++ ] ++ ]; + return (0, import_react17.createElement)( + import_react17.Fragment, null, -- (0, import_react8.createElement)( +- (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)( + (0, import_react17.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", ++ { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) - ), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ), + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.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)( +- { 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-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, + (0, import_react17.createElement)(FlyBotAccountHeader, { actions }), + groups.map((group, index) => (0, import_react17.createElement)( + import_react17.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: index }, + index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", { - key: label, - type: "button", - role: "menuitem", -@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi - 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)) ++ key: label, ++ type: "button", ++ role: "menuitem", ++ onClick: () => { ++ setOpen(false); ++ void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); ++ } + }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) - )) - ), document.body) : null - ); -@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) { - const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); - return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; - }); -- const lastBot = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { ++ )) ++ ), document.body) : null ++ ); ++} ++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_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - if (!roster.ready || !roster.mainBotId) return void 0; - const [id, blank3] = mainView.split("|"); - const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) { - return void 0; - } - const last = lastBot.current; -- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); ++ 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 last = lastBot.current; + const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), 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 })); ++ return () => clearTimeout(timer); ++ }, [mainView, roster]); ++} ++function FooterAccount(props) { ++ const seated = props.useUi((value) => value.accountSeated === true); ++ if (seated) return null; + return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.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)( ++} ++function BotReturn({ wide, useSurface, actions }) { ++ const mode = useSurface((value) => value.mode); ++ if (mode !== "agent") return null; + return (0, import_react17.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 ++ "button", + { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, + (0, import_react17.createElement)(FlyBotMark, { size: 20 }), + wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : 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} ++ ); ++} ++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_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); ++/* 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_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size })); - } - function BrandName() { -- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); ++} ++function BrandName() { + return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); - } - var SIDEBAR_CSS = ` - .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5456,7 +5456,7 @@ var SIDEBAR_CSS = ` - .bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} - @keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} - .bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.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}} ++} ++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:130;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)} -@@ -5491,18 +5491,140 @@ var SIDEBAR_CSS = ` - .bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} - @keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} - @media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} ++@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}} + +.bt-row-wrap{position:relative;display:flex;align-items:center;width:100%;min-width:0} +.bt-row-wrap>.bt-row{flex:1 1 auto;min-width:0;width:100%} @@ -12503,10 +12560,23 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-astack" }, + (0, import_react18.createElement)( -+ "div", + "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)( + { className: "bt-msg" }, + (0, import_react18.createElement)( -+ "div", + "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")) + { className: "bt-msg-line" }, + text2 ? (0, import_react18.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react18.createElement)( + "div", @@ -12515,7 +12585,10 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("span", null), + (0, import_react18.createElement)("span", null) + ) -+ ) + ) +- ) : null +- ), document.body) : null +- ); + ) + ) + ), host); @@ -12529,10436 +12602,10810 @@ diff --git a/client.js b/client.js +.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s} +@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}} +@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} - `; - - // 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" })); ++`; ++ ++// src/client/project-board.js +var import_react19 = require("react"); -+var import_react_dom6 = require("react-dom"); -+var GoArrow = () => (0, import_react19.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_react19.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 [shown, setShown] = (0, import_react19.useState)(false); -+ (0, import_react19.useEffect)(() => { - const left = seen + MEMORY_NOTICE_MS - Date.now(); - setShown(seen > 0 && left > 0); - if (seen === 0 || left <= 0) return void 0; -@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { - return shown; - } - function MemoryNotice({ shown }) { -- const inner = (0, import_react9.useRef)(null); -- const [width, setWidth] = (0, import_react9.useState)(0); -+ const inner = (0, import_react19.useRef)(null); -+ const [width, setWidth] = (0, import_react19.useState)(0); - const label = t("Memory updated"); -- (0, import_react9.useLayoutEffect)(() => { -+ (0, import_react19.useLayoutEffect)(() => { - if (shown) setWidth(inner.current?.scrollWidth ?? 0); - }, [label, shown]); -- return (0, import_react9.createElement)( -+ return (0, import_react19.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) -+ (0, import_react19.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react19.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react19.createElement)(MemoryIcon)), label) - ); - } --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) => { -@@ -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_react19.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"); -@@ -5548,52 +5548,41 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe - const bot = roster.byId[sessionId]; - const room2 = roster.roomsById[sessionId]; - const detailsOpen = useUi((state) => state.details === sessionId); -- 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 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_react19.createElement)( - "div", - { className: "bt-head" }, -- (0, import_react9.createElement)( -+ (0, import_react19.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) -+ "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_react19.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -+ (0, import_react19.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), -+ (0, import_react19.createElement)(MemoryNotice, { shown: memoryNotice }), -+ (0, import_react19.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react19.createElement)(GoArrow)) - ), -- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), -- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) -+ (0, import_react19.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -+ (0, import_react19.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), -+ (0, import_react19.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), -+ (0, import_react19.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) - ); - } - function useTranscriptView() { -- const [view, setView] = (0, import_react9.useState)(null); -- (0, import_react9.useEffect)(() => { -+ const [view, setView] = (0, import_react19.useState)(null); -+ (0, import_react19.useEffect)(() => { - let frame = 0; - let watched = null; - const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +5642,7 @@ function useTranscriptView() { - }, []); - return view; - } --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; -@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - }); - 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_react19.useState)({ base: null, dismissed: false }); -+ const [above, setAbove] = (0, import_react19.useState)({ seen: false, dismissed: false }); -+ (0, import_react19.useEffect)(() => { - setRelease({ base: unpinned ? replies : null, dismissed: false }); - }, [unpinned]); -- (0, import_react9.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { - if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); - }, [view?.divider]); -+ const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); -+ (0, import_react19.useEffect)(() => { -+ if (above.seen && movedOn) actions?.settleVisit?.(sessionId); -+ }, [above.seen, movedOn, sessionId]); - const fresh = release.base === null ? 0 : replies - release.base; - const showDown = unpinned && fresh > 0 && !release.dismissed; - const showUp = view?.divider === "above" && !above.seen && !above.dismissed; -- (0, import_react9.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { - const scroller = document.querySelector("[data-conversation-scroll]"); - if (!scroller) return void 0; - scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - }, [showDown]); - if (!view || !showDown && !showUp) return null; - const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); -- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( -+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react19.createElement)( - "div", - { className: "bt-news", "data-direction": direction, style }, -- (0, import_react9.createElement)( -+ (0, import_react19.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)), -+ (0, import_react19.createElement)("span", { className: "bt-news-ico" }, (0, import_react19.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), - label(count) - ), -- (0, import_react9.createElement)("button", { -+ (0, import_react19.createElement)("button", { - type: "button", - className: "bt-news-x", - "aria-label": t("Dismiss new messages"), -@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - event.stopPropagation(); - onDismiss(); - } -- }, (0, import_react9.createElement)(CloseIcon)) -+ }, (0, import_react19.createElement)(CloseIcon)) - ); - const toLatest = () => { - const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5703,9 +5703,9 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); - }; - const 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_react19.createElement)( -+ import_react19.Fragment, - null, - showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, - showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -@@ -5714,19 +5714,135 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - ); - } - -+// src/client/composer-label.js -+var ATTRIBUTES = ["aria-label", "data-placeholder"]; -+function holdComposerLabel(doc, label) { -+ doc.body.style.setProperty("--bt-placeholder", JSON.stringify(label)); -+ const shell = /* @__PURE__ */ new Map(); -+ const apply2 = () => { -+ for (const el of doc.querySelectorAll("[data-composer-input]")) { -+ for (const name of ATTRIBUTES) { -+ const current = el.getAttribute(name); -+ if (current === label) continue; -+ if (!shell.has(el)) shell.set(el, {}); -+ shell.get(el)[name] = current; -+ el.setAttribute(name, label); -+ } -+ } -+ }; -+ apply2(); -+ const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver; -+ const watcher = typeof Observer === "function" ? new Observer(apply2) : null; -+ watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES }); -+ return () => { -+ watcher?.disconnect(); -+ for (const [el, values] of shell) { -+ if (!el.isConnected) continue; -+ for (const [name, value] of Object.entries(values)) { -+ if (value == null) el.removeAttribute(name); -+ else el.setAttribute(name, value); -+ } -+ } -+ }; -+} ++var PROJECT_BOARD_CSS = ` ++/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the ++ bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the ++ Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */ ++.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))} ++.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto} ++.bt-project-board{padding:16px 18px} ++.bt-project-group{margin:0 0 14px} ++.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} ++.bt-project-row[data-column="done"]{opacity:.7} ++.bt-project-title-wrap{display:flex;flex-direction:column;gap:2px;min-width:0} ++.bt-project-audit{font-size:11px;line-height:1.45;white-space:pre-wrap;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} ++.bt-project-empty{padding:22px 4px;opacity:.7} ++/* #1767: the record beside the cards. The same surface as a row, one step quieter, and no new ++ component language: a heading, a list, and a monospace hash the reader can compare. */ ++.bt-project-record{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} ++.bt-project-record h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-record h5{margin:10px 0 4px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;opacity:.55} ++.bt-project-decision,.bt-project-meeting{display:flex;gap:10px;align-items:baseline;padding:6px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} ++.bt-project-decision .bt-project-date,.bt-project-meeting .bt-project-date{font-size:11px;opacity:.6;min-width:76px;font-variant-numeric:tabular-nums} ++.bt-project-decision .bt-project-hash{margin-left:auto;font-size:11px;opacity:.55;font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-decision .bt-project-title,.bt-project-meeting .bt-project-title{min-width:0} ++.bt-project-path{font-size:11px;opacity:.6;font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-missing{font-size:11px;opacity:.6} ++.bt-project-status{font-size:11px;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-left:auto} ++/* #1831: the card's level, the people it is addressed to, and the identity a row or a comment ++ carries. A severe card is the only one painted louder, because it is the only one that waits. */ ++.bt-project-level{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));opacity:.75} ++.bt-project-level[data-level="severe"]{opacity:1;border-color:#c2410c;color:#c2410c} ++.bt-project-recipients{font-size:11px;opacity:.65} ++.bt-project-paused{font-size:11px;opacity:.75;color:#c2410c} ++.bt-project-holder{font-size:11px;opacity:.7} ++.bt-project-speaks-for{font-size:11px;opacity:.7;font-style:italic} ++.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} ++.bt-project-metrics{margin:0 0 14px} ++.bt-project-metric-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:8px} ++.bt-project-metric{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border:1px solid rgba(127,127,127,.24);border-radius:8px} ++.bt-project-metric-value{font-size:18px;font-weight:600} ++.bt-project-metric-label{font-size:12px;font-weight:600} ++.bt-project-metric-caption{font-size:10px;opacity:.6;line-height:1.3} ++.bt-project-metrics-note{margin:0 0 6px;font-size:11px;opacity:.7} ++.bt-project-running{margin:0 0 14px} ++.bt-project-running-head,.bt-project-running-row{display:grid;grid-template-columns:2fr 1fr 1fr .8fr .8fr 1.2fr;gap:8px;font-size:12px;padding:3px 0} ++.bt-project-running-head{opacity:.6;font-size:10px;letter-spacing:.03em} ++.bt-project-running-row[role="button"]{cursor:pointer} ++.bt-project-running-row[data-overdue="true"]{color:#c2410c} ++.bt-project-counts{display:flex;gap:14px;align-items:center;margin:0 0 12px;font-size:12px;letter-spacing:.03em;opacity:.8} ++.bt-project-counts b{font-weight:600} ++.bt-project-group[data-queue="waiting"] .bt-project-row{cursor:default} ++.bt-project-role{font-size:11px;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-wait{font-size:11px;opacity:.65;margin-left:auto} ++.bt-project-state{font-size:11px;margin-left:auto} ++.bt-project-row[data-state="stalled"]{border-left:2px solid #d9822b} ++.bt-project-reason{font-size:11px;opacity:.7} + -+// src/client/header-effects.js -+function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { -+ if (!doc) return () => { -+ }; -+ const slots = /* @__PURE__ */ new Map(); -+ const run = (id, dep, setup) => { -+ const held = slots.get(id); -+ if (held !== void 0 && held.dep === dep) return; -+ held?.off(); -+ slots.set(id, { dep, off: setup() ?? (() => { -+ }) }); ++/* #1855: the finished cards are one group of their own, closed until the reader opens it — a ++ Project with months of history reads as the work in flight, not as its archive. */ ++.bt-project-done{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} ++.bt-project-done>summary{cursor:pointer;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7;list-style:none} ++.bt-project-done>summary::-webkit-details-marker{display:none} ++.bt-project-done>summary::before{content:'\\25B8';display:inline-block;width:12px;opacity:.6} ++.bt-project-done[open]>summary::before{content:'\\25BE'} ++.bt-project-done[open]>summary{margin:0 0 8px} ++ ++/* #1749: management panels. Same surface as the board, one form row per control, and a status ++ line that quotes the host's own answer — a refused save reads where the reader pressed it. */ ++.bt-project-panel{padding:16px 18px;display:flex;flex-direction:column;gap:12px} ++.bt-project-panel h3{margin:0} ++.bt-project-field{display:flex;flex-direction:column;gap:4px} ++.bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} ++.bt-project-field textarea{min-height:88px;resize:vertical} ++/* #1887: the counter under the memory box. It stays quiet until the text passes the bound, and ++ past it turns the one colour that says so; the disabled Save is the state a reader acts on. */ ++.bt-project-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums} ++.bt-project-count[data-over="true"]{opacity:1;color:#c2410c} ++.bt-project-row-actions{display:flex;gap:8px;align-items:center} ++.bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} ++.bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} ++.bt-project-member .bt-project-member-kind{margin-left:auto;font-size:11px;opacity:.6} ++.bt-project-status-line{font-size:12px;opacity:.75;min-height:16px} ++.bt-project-note{font-size:12px;opacity:.65;margin:0} ++ ++/* #1860: the Project team — who runs the Project, who speaks for the reader, and the role pool. ++ One quiet block above the cards, and a Bot row is a button because a reader is looking a ++ person up. The pool row keeps the rank it plays and the ranks the host said it may switch to. */ ++.bt-project-team{margin:0 0 14px} ++.bt-project-team h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-team-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} ++.bt-project-team-row[data-open-bot]{cursor:pointer} ++.bt-project-team-row[data-open-bot]:hover{background:var(--bt-hover,rgba(127,127,127,.14))} ++.bt-project-team-label{font-size:12px;opacity:.7;min-width:96px} ++.bt-project-team-bot{font-size:12px;font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-team-roles{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.75} ++.bt-project-team-role{font-family:var(--bt-mono,ui-monospace,monospace)} ++.bt-project-team-myrole{margin-left:auto;font-size:11px;opacity:.65} ++ ++/* #1864: the team dossier. A Dossier control on each team row opens one read-only panel; the ++ owner's own chat header carries the same entry to the Project assistant's chat. */ ++.bt-project-team-dossier{margin-left:auto;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:transparent;color:inherit;cursor:pointer} ++.bt-project-team-dossier:hover{background:var(--bt-hover,rgba(127,127,127,.14))} ++.bt-project-dossier{padding:16px 18px;display:flex;flex-direction:column;gap:12px} ++.bt-project-dossier-section{display:flex;flex-direction:column;gap:4px} ++.bt-project-dossier-section h4{margin:0;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-dossier-text{margin:0;font-size:13px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere} ++.bt-project-dossier-meta{display:flex;gap:12px;flex-wrap:wrap;font-size:11px;opacity:.7} ++.bt-project-dossier-topics{display:flex;gap:6px;flex-wrap:wrap} ++.bt-project-dossier-topic{font-size:11px;padding:1px 6px;border-radius:999px;background:var(--bt-card,rgba(127,127,127,.08))} ++.bt-project-agent-entry{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:transparent;color:inherit;cursor:pointer} ++.bt-project-agent-entry:hover{background:var(--bt-hover,rgba(127,127,127,.14))} ++`; ++var PROJECT_SOURCES = ["bot", "group", "workflow"]; ++var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; ++var text = (value) => typeof value === "string" ? value.trim() : ""; ++var MEMORY_TEXT_MAX = 600; ++var memoryLength = (value) => [...text(value)].length; ++var DONE_COLUMN = "done"; ++var boardOfRow = (row, task) => { ++ if (task === void 0) return null; ++ if (row.source === "group") { ++ const id = text(task.origin?.roomId); ++ return id === "" ? null : { kind: "group", id }; ++ } ++ if (row.source === "bot") { ++ const id = text(task.ownerBotId); ++ return id === "" ? null : { kind: "bot", id }; ++ } ++ return null; ++}; ++var decisionOf = (file) => { ++ if (file === null || typeof file !== "object") return null; ++ const path = text(file.path); ++ if (path === "") return null; ++ return { ++ key: `decision:${path}`, ++ path, ++ name: text(file.name) || path.slice(path.lastIndexOf("/") + 1), ++ date: text(file.date), ++ title: text(file.title), ++ sha256: text(file.sha256), ++ shortHash: text(file.sha256).slice(0, 12) + }; -+ let running = false; -+ let again = false; -+ const pass = () => { -+ const list = sessions.getSnapshot(); -+ const state = roster.getSnapshot(); -+ const row = Object.values(list?.byId ?? {}).find((entry) => (entry?.retainedBy?.mainView ?? 0) > 0); -+ const sessionId = row?.id; -+ const bot = sessionId === void 0 ? void 0 : state?.byId?.[sessionId]; -+ const room2 = sessionId === void 0 ? void 0 : state?.roomsById?.[sessionId]; -+ const name = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; -+ run("friend-chat", `${sessionId}|${bot?.remote === true}`, () => { -+ if (!bot?.remote) return void 0; -+ doc.body.dataset.btFriendChat = ""; -+ return () => { -+ delete doc.body.dataset.btFriendChat; -+ }; -+ }); -+ run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { -+ if (!bot?.remote || row?.blank !== true) return void 0; -+ doc.body.dataset.btRemoteChat = "empty"; -+ doc.body.style.setProperty("--bt-remote-empty", JSON.stringify(t("Send a message. Your friend approves unless Auto-send is on."))); -+ return () => { -+ delete doc.body.dataset.btRemoteChat; -+ doc.body.style.removeProperty("--bt-remote-empty"); -+ }; -+ }); -+ run("composer-label", `${sessionId}|${name}`, () => holdComposerLabel(doc, name ? t("Message {name}", { name }) : t("Message"))); -+ run("visit", sessionId, () => { -+ if (sessionId === void 0) return void 0; -+ actions.beginVisit(sessionId); -+ return () => actions.endVisit(sessionId); -+ }); -+ run("face-at-menu", `${sessionId}|${room2?.origin === "face-to-face"}`, () => { -+ if (room2?.origin !== "face-to-face") return void 0; -+ const stop = actions.filterFaceAtMenu?.(sessionId); -+ return typeof stop === "function" ? stop : void 0; -+ }); -+ run("room-flag", `${sessionId}|${room2 !== void 0}`, () => { -+ if (room2 === void 0) return void 0; -+ doc.body.dataset.btRoom = ""; -+ return () => { -+ delete doc.body.dataset.btRoom; -+ }; -+ }); ++}; ++var meetingOf = (meeting, status) => { ++ if (meeting === null || typeof meeting !== "object") return null; ++ const id = text(meeting.id); ++ if (id === "") return null; ++ return { ++ key: `meeting:${id}`, ++ id, ++ date: text(meeting.date), ++ question: text(meeting.question) || text(meeting.title), ++ owner: text(meeting.owner) || null, ++ status, ++ path: text(meeting.path), ++ blockingQuestion: text(meeting.blockingQuestion) || null, ++ // #1831: the level and recipients the host answered, and the refusal to advance a severe card. ++ // Nothing is inferred here: a record written before the slice carries an ordinary card and no ++ // recipients, exactly as the host reads it. ++ level: text(meeting.level) || "ordinary", ++ recipients: Array.isArray(meeting.recipients) ? meeting.recipients.map((item) => text(item)).filter((name) => name !== "") : [], ++ advance: meeting.advance !== null && typeof meeting.advance === "object" ? { allowed: meeting.advance.allowed === true, reason: text(meeting.advance.reason) || null } : { allowed: true, reason: null }, ++ // #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 apply2 = () => { -+ if (running) { -+ again = true; -+ return; -+ } -+ running = true; -+ try { -+ pass(); -+ } finally { -+ running = false; -+ } -+ if (again) { -+ again = false; -+ apply2(); ++}; ++var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; ++function projectBoardRecord({ decisions, meetings } = {}) { ++ const dir = text(decisions?.dir) || null; ++ const files = listOf(decisions?.files); ++ const decided = Array.isArray(meetings?.decided) ? meetings.decided.map((item) => meetingOf(item, "decided")).filter((item) => item !== null) : []; ++ const pending = Array.isArray(meetings?.pending) ? meetings.pending.map((item) => meetingOf(item, item?.status ?? "deliberating")).filter((item) => item !== null) : []; ++ return { ++ decisions: { ++ dir, ++ files, ++ count: files.length, ++ empty: files.length === 0, ++ emptyText: text(decisions?.reason) || (dir === null ? "This Project names no working folder." : `No decision file in ${dir}/decisions.`) ++ }, ++ meetings: { ++ decided, ++ pending, ++ count: decided.length + pending.length, ++ empty: decided.length === 0 && pending.length === 0, ++ emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." + } + }; -+ apply2(); -+ const offSessions = sessions.subscribe(apply2); -+ const offRoster = roster.subscribe(apply2); -+ return () => { -+ offSessions?.(); -+ offRoster?.(); -+ for (const held of slots.values()) held.off(); -+ slots.clear(); -+ }; -+} -+ - // src/client/composer.js --var import_react10 = require("react"); --var import_react_dom5 = require("react-dom"); -+var import_react20 = require("react"); -+var import_react_dom7 = require("react-dom"); - var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; - var keepComposerFocus = (event) => event.preventDefault(); - function useDictation(onText) { -- const [listening, setListening] = (0, import_react10.useState)(false); -- const [note, setNote] = (0, import_react10.useState)(""); -- const recognition = (0, import_react10.useRef)(null); -- const latest = (0, import_react10.useRef)(onText); -+ const [listening, setListening] = (0, import_react20.useState)(false); -+ const [note, setNote] = (0, import_react20.useState)(""); -+ const recognition = (0, import_react20.useRef)(null); -+ const latest = (0, import_react20.useRef)(onText); - latest.current = onText; -- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); -- (0, import_react10.useEffect)(() => { -+ (0, import_react20.useEffect)(() => () => recognition.current?.abort(), []); -+ (0, import_react20.useEffect)(() => { - if (!note) return void 0; - const timer = setTimeout(() => setNote(""), 2600); - return () => clearTimeout(timer); -@@ -5745,10 +5745,10 @@ function useDictation(onText) { - let interim = ""; - for (let index = event.resultIndex; index < event.results.length; index += 1) { - const result = event.results[index]; -- const text = result[0]?.transcript ?? ""; -+ const text2 = result[0]?.transcript ?? ""; - 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); - }; -@@ -5774,19 +5774,38 @@ function useDictation(onText) { - const stop = () => recognition.current?.stop(); - return { listening, note, start, stop }; } -+function ComposerIdentity({ sessionId, useRoster }) { -+ const roster = useRoster((value) => value); -+ const room2 = roster.roomsById?.[sessionId]; -+ const self = roster.self; -+ const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ if (room2?.viewerRole === "invited-owner") { -+ return (0, import_react20.createElement)( -+ "span", -+ { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, -+ t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) -+ ); +-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 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])); ++ const list = []; ++ const archived = []; ++ const seen = /* @__PURE__ */ new Set(); ++ for (const row of [...Array.isArray(done) ? done : [], ...Array.isArray(rows) ? rows : []]) { ++ if (!isRow(row)) continue; ++ const id = text(row.id); ++ if (seen.has(id)) continue; ++ seen.add(id); ++ const column = text(row.column) || "not-started"; ++ (column === DONE_COLUMN ? archived : list).push(row); + } -+ if (!self || !room2 || humans.length < 2) return null; -+ return (0, import_react20.createElement)( -+ "span", -+ { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, -+ t("Sending as {name}", { name: self.displayName || self.memberId }) -+ ); -+} - function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -- const dictation = useDictation((text, final) => { -- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); -+ const dictation = useDictation((text2, final) => { -+ if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); - }); - const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -- (0, import_react10.useEffect)(() => { -+ (0, import_react20.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} -+ const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} - - `; -- inputActions.insertText(line, inputActions.captureInsertion()); -+ inputActions.insertText(line2, inputActions.captureInsertion()); - const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); - if (box instanceof HTMLElement) { - box.focus(); -@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - }, [quote?.token]); - if (sessionId === void 0) return null; - const pluginMic = actions.speech() !== void 0; -- return (0, import_react10.createElement)( -- import_react10.Fragment, -+ return (0, import_react20.createElement)( -+ import_react20.Fragment, - null, -- pluginMic ? null : (0, import_react10.createElement)("button", { -+ pluginMic ? null : (0, import_react20.createElement)("button", { - type: "button", - className: "bt-mic", - "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - title: dictation.listening ? t("Stop dictation") : t("Dictate"), - onMouseDown: keepComposerFocus, - onClick: () => dictation.listening ? dictation.stop() : dictation.start() -- }, (0, import_react10.createElement)(MicIcon)), -- (0, import_react10.createElement)("button", { -+ }, (0, import_react20.createElement)(MicIcon)), -+ (0, import_react20.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 -+ }, (0, import_react20.createElement)(WaveIcon, { size: 18 })), -+ dictation.note ? (0, import_react20.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null - ); - } - function menuIcons(roster) { -@@ -5825,17 +5825,17 @@ function menuIcons(roster) { - return (row) => { - if (!row.bot) return PeopleIcon; - const id = row.bot.id; -- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); -+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react20.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); - return marks.get(id); - }; - } - function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - const roster = useRoster((value) => value); -- const [open, setOpen] = (0, import_react10.useState)(null); -+ const [open, setOpen] = (0, import_react20.useState)(null); - const [shown, leaving] = useLinger(open, 120); -- const button = (0, import_react10.useRef)(null); -- const menu = (0, import_react10.useRef)(null); -- (0, import_react10.useEffect)(() => { -+ const button = (0, import_react20.useRef)(null); -+ const menu = (0, import_react20.useRef)(null); -+ (0, import_react20.useEffect)(() => { - if (!open) return void 0; - const onDown = (event) => { - if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); ++ const projectRow = (row) => { ++ const task = byId.get(text(row.id)); ++ return { ++ id: text(row.id), ++ key: `task:${text(row.id)}`, ++ column: text(row.column) || "not-started", ++ assignee: text(row.assignee) || null, ++ runId: text(row.runId) || null, ++ nodeId: text(row.nodeId) || null, ++ title: text(task?.title) || text(task?.note) || null, ++ priority: text(task?.priority) || null, ++ audit: text(row.audit) || null, ++ // #1831: who is doing the card, and who is speaking for whom. Both are host truth; ++ // a card carrying neither renders exactly as before. ++ holder: text(row.holder) || null, ++ speaksFor: text(row.speaksFor) || null, ++ board: boardOfRow(row, task) ++ }; }; - const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); -- return (0, import_react10.createElement)( -- import_react10.Fragment, -+ return (0, import_react20.createElement)( -+ import_react20.Fragment, - null, -- (0, import_react10.createElement)("button", { -+ (0, import_react20.createElement)("button", { - ref: button, - type: "button", - className: "bt-plus", -@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - "aria-expanded": Boolean(open), - onMouseDown: keepComposerFocus, - onClick: toggle -- }, (0, import_react10.createElement)(PlusIcon)), -- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( -+ }, (0, import_react20.createElement)(PlusIcon)), -+ shown ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)( - "div", - { - ref: menu, -@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - style: { left: shown.left, bottom: shown.bottom }, - "data-leaving": leaving || void 0 - }, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react20.createElement)(PaperclipIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -+ canReference ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react20.createElement)(ChatsIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react20.createElement)(ClockIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) - ), document.body) : null +- const hide = () => { +- clearTimeout(timer.current); +- setTip(false); ++ const groups = PROJECT_SOURCES.map((source) => ({ ++ source, ++ label: SOURCE_LABELS[source], ++ rows: list.filter((row) => row.source === source).map(projectRow) ++ })).filter((group) => group.rows.length > 0); ++ const countOf = (value) => value === void 0 || value === null ? null : value; ++ const queueItems = (items, annotate) => (Array.isArray(items) ? items : []).filter((item) => item !== null && typeof item === "object" && text(item.id) !== "").map((item) => { ++ const id = text(item.id); ++ const source = text([...list, ...archived].find((row) => text(row.id) === id)?.source); ++ const task = byId.get(id); ++ return { id, key: `task:${id}`, title: text(task?.title) || text(task?.note) || null, priority: text(task?.priority) || null, board: boardOfRow({ source }, task), ...annotate(item) }; ++ }); ++ const waiting = queueItems(queues?.waiting, (item) => ({ role: text(item.role) || null, waitedMs: Number.isFinite(item.waitedMs) ? item.waitedMs : null })); ++ const inflight = queueItems(queues?.inflight, (item) => ({ holder: text(item.holder) || null, state: item.stalled === true ? "stalled" : "running" })); ++ const blocked = queueItems(queues?.blocked, (item) => ({ reason: text(item.reason) || null })); ++ const queueGroups = [ ++ { group: "waiting", label: "Waiting", items: waiting }, ++ { group: "inflight", label: "In flight", items: inflight }, ++ { group: "blocked", label: "Blocked", items: blocked } ++ ].filter((entry) => entry.items.length > 0); ++ const rawCounts = queues?.counts ?? null; ++ return { ++ projectId: text(project?.id) || null, ++ title: text(project?.name) || "Project", ++ canManage: canManage === true, ++ groups, ++ // #1855: one group, closed until the reader opens it. Rows keep the row shape above, so a ++ // finished card is still a card — only its place on the board changed. ++ done: { group: "done", label: "Done cards", collapsed: true, rows: archived.map(projectRow) }, ++ queues: queueGroups, ++ counts: rawCounts === null ? null : { waiting: countOf(rawCounts.waiting), inflight: countOf(rawCounts.inflight), blocked: countOf(rawCounts.blocked), done: countOf(rawCounts.done) }, ++ count: list.length, ++ empty: list.length === 0 && queueGroups.length === 0 && archived.length === 0, ++ emptyText: "No cards yet.", ++ record: projectBoardRecord({ decisions, meetings }), ++ metrics: metricsView(metrics, { tasks }) + }; +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-connect-wrap" }, +- (0, import_react8.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") +- }, +- (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)( ++} ++function projectRowNode(row, onOpen) { ++ const open = row.board === null ? void 0 : () => onOpen?.(row.board); ++ return (0, import_react19.createElement)( ++ "div", ++ { ++ key: row.key, ++ className: "bt-project-row", ++ "data-task-id": row.id, ++ "data-column": row.column, ++ "data-priority": row.priority ?? void 0, ++ role: open === void 0 ? void 0 : "button", ++ tabIndex: open === void 0 ? void 0 : 0, ++ onClick: open, ++ onKeyDown: open === void 0 ? void 0 : (event) => { ++ if (event.key === "Enter" || event.key === " ") open(); ++ } ++ }, ++ (0, import_react19.createElement)("span", { className: "bt-project-column" }, t(row.column)), ++ (0, import_react19.createElement)( + "span", +- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, +- t("Plugin compatibility is not fully verified.") +- ), document.body) : null ++ { className: "bt-project-title-wrap" }, ++ (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), ++ // 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_react19.createElement)("span", { className: "bt-project-audit" }, row.audit) ++ ), ++ // "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_react19.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), ++ row.speaksFor === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), ++ row.priority === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-priority" }, row.priority), ++ row.runId === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) ); } -@@ -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 ? [ -+ ...here.room.members.map((id) => roster.byId[id]).filter(Boolean).map((bot) => face ? { ...bot, label: faceLabel(myOwnerName(roster, live, bot), bot.name) } : bot), -+ ...live.filter((p) => p.kind === "human").map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role })), -+ // A remote seat already listed under its handle (a duplicate participant record) is offered once. -+ ...uniqueBy(live.filter((p) => p.remote), (p) => p.handle || p.id).map((p) => ({ -+ id: p.handle || p.id, -+ // A face room inserts the disambiguated title. An ordinary group inserts the name the -+ // room routes on (#1141): never "@@handle.home (owner)" and never "(Friend)". -+ name: face ? remoteBotTitle(p) : remoteCallName(p), -+ kind: "bot", -+ remote: p, -+ handle: p.handle, -+ label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) -+ })) -+ ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); - const needle = query.trim().toLowerCase(); -- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); -+ const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); - const everyone = t("Everyone"); - if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -- rows.push({ room: here.room, text: `@${everyone} ` }); -+ 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)))]; -+ const remoteHandles = new Set(remoteMentionRows(roster, "").map((row) => normHandle(row.remote.handle))); -+ return [ -+ ...rows.filter((row) => !(row.bot && remoteHandles.has(normHandle(remoteRecord(row.bot)?.handle)))), -+ ...remoteMentionRows(roster, query) -+ ]; +-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")) ++function ProjectBoardRows({ view, onOpen }) { ++ if (view.empty) return (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); ++ return view.groups.map((group) => (0, import_react19.createElement)( ++ "section", ++ { key: group.source, className: "bt-project-group", "data-source": group.source }, ++ (0, import_react19.createElement)("h4", null, t(group.label)), ++ group.rows.map((row) => projectRowNode(row, onOpen)) ++ )); ++} ++function ProjectDoneGroup({ view, onOpen }) { ++ if (view.done.rows.length === 0) return null; ++ return (0, import_react19.createElement)( ++ "details", ++ { className: "bt-project-group bt-project-done", "data-group": "done" }, ++ (0, import_react19.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), ++ view.done.rows.map((row) => projectRowNode(row, onOpen)) + ); } -+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 }); -+var normHandle = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); -+var remoteRecord = (bot) => bot?.remote ? bot.remote === true ? bot : bot.remote : null; -+var myOwnerName = (roster, live, bot) => { -+ const seat = live.find((p) => !p.remote && p.kind !== "human" && (p.id === bot.id || bot.handle && p.handle === bot.handle)); -+ return seatOwnerName(seat?.ownerName, seat?.handle) || cleanPeerName(roster.self?.displayName, ""); -+}; -+var uniqueBy = (list, key) => { -+ const seen = /* @__PURE__ */ new Set(); -+ return list.filter((item) => { -+ const k = key(item); -+ if (!k || seen.has(k)) return !k; -+ seen.add(k); -+ return true; -+ }); +-var openShellSettings = null; +-var openShellOnboarding = null; +-var shellShortcuts = null; +-function holdShellShortcuts(shortcuts) { +- shellShortcuts = shortcuts; +- return () => { +- if (shellShortcuts === shortcuts) shellShortcuts = null; ++function waitedLabel(ms) { ++ if (!Number.isFinite(ms) || ms < 0) return null; ++ const minutes = Math.floor(ms / 6e4); ++ if (minutes < 60) return `${minutes}m`; ++ const hours = Math.floor(minutes / 60); ++ if (hours < 24) return `${hours}h`; ++ return `${Math.floor(hours / 24)}d`; ++} ++var METRIC_CARDS = [ ++ { key: "prsOpenedToday", label: "PRs opened today", caption: "Natural day in your time zone, by created_at." }, ++ { key: "prsMergedToday", label: "PRs merged today", caption: "Natural day in your time zone, by merged_at." }, ++ { key: "issuesOpenedToday", label: "Issues opened today", caption: "Natural day in your time zone, by created_at." }, ++ { key: "issuesClosedToday", label: "Issues closed today", caption: "Natural day in your time zone, by closed_at." }, ++ { key: "prMergeMedianMs", label: "PR merge time", caption: "Median of the last 7 days, opened to merged." }, ++ { key: "issueCloseMedianMs", label: "Issue close time", caption: "Median of the last 7 days, opened to closed." }, ++ { key: "taskDoneMedianMs", label: "Task completion", caption: "Median of the last 7 days, created to done." } ++]; ++var METRIC_DASH = "—"; ++var DURATION_KEYS = /* @__PURE__ */ new Set(["prMergeMedianMs", "issueCloseMedianMs", "taskDoneMedianMs"]); ++var RUNNING_OVERDUE_MS = 2 * 60 * 60 * 1e3; ++var metricText = (key, value) => { ++ if (!Number.isFinite(value)) return METRIC_DASH; ++ if (DURATION_KEYS.has(key)) return waitedLabel(value) ?? METRIC_DASH; ++ return String(value); +}; -+var faceLabel = (person, bot) => person ? `${bot} (${person})` : bot; - function chatRows(roster, list, sessionId) { - const here = conversationOf(roster, sessionId); - if (here === null) return []; -@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { - order: -20, - showGroupTitle: false, - async candidates(session, { query }) { -- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ -- name: row.bot ? row.bot.name : t("Everyone"), -- ...row.bot?.role ? { description: row.bot.role } : {}, -- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, -+ const snapshot = roster.getSnapshot(); -+ const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); -+ const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ -+ // A friend's Bot by the one name the row inserts, "Chief (Bob)" (#967); its handle is the -+ // second line (#384). A seat carries its own name; a friend's chat is named as its sidebar -+ // row and header (#498). -+ name: row.bot?.remote && !face ? row.bot.kind === "bot" ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? row.text.replace(/^@/, "").trimEnd() : t("Everyone"), -+ ...row.bot?.role && !face ? { description: row.bot.role } : {}, -+ ...row.remote ? { description: row.remote.handle } : {}, -+ ...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 - })); -+ if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; -+ const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ -+ name: row.name, -+ ...row.description ? { description: row.description } : {}, -+ icon: row.icon ?? "session", -+ section: row.section ?? "FlyBot", -+ value: row.value -+ })); -+ return [...team, ...extra]; - }, - onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 ++function metricsView(metrics, { tasks } = {}) { ++ const source = metrics !== null && typeof metrics === "object" ? metrics : null; ++ const values = source?.metrics !== null && typeof source?.metrics === "object" ? source.metrics : {}; ++ const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); ++ const running = (Array.isArray(source?.running) ? source.running : []).filter((row) => row !== null && typeof row === "object" && text(row.id) !== "").map((row) => { ++ const id = text(row.id); ++ const task = byId.get(id); ++ return { ++ id, ++ key: "task:" + id, ++ title: text(row.title) || text(task?.title) || text(task?.note) || id, ++ status: text(row.status), ++ who: text(row.who) || null, ++ sinceMs: Number.isFinite(row.sinceMs) ? row.sinceMs : null, ++ source: text(row.source) || null, ++ 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) ++ }; ++ }); ++ return { ++ available: source?.ok === true, ++ reason: text(source?.reason) || null, ++ cards: METRIC_CARDS.map((card) => ({ ...card, value: metricText(card.key, values[card.key]) })), ++ running, ++ empty: source === null, ++ emptyText: "No metrics read yet." }; -@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { - return [bots, chats]; } - -+// src/client/face-at-filter.js -+var REFERENCE_SOURCE = "reference"; -+var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; -+var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; -+var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; -+function planFaceMenu(state, previous = null) { -+ const hidden = []; -+ const ready = []; -+ let shown = 0; -+ let loading = false; -+ for (const group of state?.groups ?? []) { -+ const items = group.items ?? []; -+ if (group.status !== "ready" && items.length === 0) loading = true; -+ items.forEach((item, index) => { -+ const hide = isSessionRow(group, item); -+ if (hide) hidden.push({ source: group.source, index, title: item.section !== void 0 && item.section !== items[index - 1]?.section }); -+ else shown += 1; -+ if (group.status === "ready") ready.push({ source: group.source, index, hide }); -+ }); -+ } -+ const open = state?.open === true; -+ const blank3 = open && hidden.length > 0 && shown === 0 && !loading; -+ const dismiss = blank3 && (state.groups ?? []).every((group) => group.status === "ready"); -+ return { hidden, blank: blank3, dismiss, target: dismiss ? null : steer(ready, state?.highlight ?? null, previous) }; -+} -+function steer(ready, highlight, previous) { -+ const at = ready.findIndex((row) => same(row, highlight)); -+ if (at < 0 || !ready[at].hide) return null; -+ const from = ready.findIndex((row) => same(row, previous)); -+ const n = ready.length; -+ const dir = from >= 0 && from !== at && (from - at + n) % n === 1 ? -1 : 1; -+ for (let step = 1; step < n; step += 1) { -+ const row = ready[((at + dir * step) % n + n) % n]; -+ if (!row.hide) return { source: row.source, index: row.index }; -+ } -+ return 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 ProjectMetrics({ view }) { ++ return (0, import_react19.createElement)( ++ "section", ++ { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, ++ view.available || view.reason === null ? null : (0, import_react19.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-metric-cards" }, ++ view.cards.map((card) => (0, import_react19.createElement)( ++ "div", ++ { key: card.key, className: "bt-project-metric", "data-metric": card.key }, ++ (0, import_react19.createElement)("span", { className: "bt-project-metric-value" }, card.value), ++ (0, import_react19.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), ++ (0, import_react19.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) ++ )) ++ ) ++ ); + } +-function openHarnessOnboarding(id) { +- requestAnimationFrame(() => { +- if (openShellOnboarding) openShellOnboarding(id); +- else openHarnessSettings(); +- }); ++function ProjectRunningTable({ view, onOpen }) { ++ if (view.running.length === 0) return (0, import_react19.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react19.createElement)("h4", null, t("Running tasks")), (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); ++ return (0, import_react19.createElement)( ++ "section", ++ { className: "bt-project-running", "data-running": String(view.running.length) }, ++ (0, import_react19.createElement)("h4", null, t("Running tasks")), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-running-head" }, ++ (0, import_react19.createElement)("span", null, t("Task")), ++ (0, import_react19.createElement)("span", null, t("Status")), ++ (0, import_react19.createElement)("span", null, t("Who")), ++ (0, import_react19.createElement)("span", null, t("Claimed for")), ++ (0, import_react19.createElement)("span", null, t("Source")), ++ (0, import_react19.createElement)("span", null, t("Last activity")) ++ ), ++ view.running.map((row) => { ++ const open = row.board === null ? void 0 : () => onOpen?.(row.board); ++ return (0, import_react19.createElement)( ++ "div", ++ { ++ key: row.key, ++ className: "bt-project-running-row", ++ "data-task-id": row.id, ++ "data-overdue": row.overdue ? "true" : "false", ++ role: open === void 0 ? void 0 : "button", ++ tabIndex: open === void 0 ? void 0 : 0, ++ onClick: open ++ }, ++ (0, import_react19.createElement)("span", { className: "bt-project-running-title" }, row.title), ++ (0, import_react19.createElement)("span", { className: "bt-project-running-status" }, t(row.status)), ++ (0, import_react19.createElement)("span", null, row.who ?? METRIC_DASH), ++ (0, import_react19.createElement)("span", null, metricText("since", row.sinceMs)), ++ (0, import_react19.createElement)("span", null, row.source === null ? METRIC_DASH : t(SOURCE_LABELS[row.source] ?? row.source)), ++ (0, import_react19.createElement)("span", null, row.lastActivityAt === null ? METRIC_DASH : new Date(row.lastActivityAt).toLocaleString(dateLocale())) ++ ); ++ }) ++ ); + } +-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(); ++function ProjectQueueGroups({ view, onOpen }) { ++ return view.queues.map((entry) => (0, import_react19.createElement)( ++ "section", ++ { key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group }, ++ (0, import_react19.createElement)("h4", null, t(entry.label)), ++ entry.items.map((row) => { ++ const open = row.board === null ? void 0 : () => onOpen?.(row.board); ++ const note = entry.group === "waiting" ? (0, import_react19.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react19.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react19.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react19.createElement)("span", { className: "bt-project-reason" }, row.reason); ++ return (0, import_react19.createElement)( ++ "div", ++ { ++ key: row.key, ++ className: "bt-project-row", ++ "data-task-id": row.id, ++ "data-state": row.state ?? void 0, ++ role: open === void 0 ? void 0 : "button", ++ tabIndex: open === void 0 ? void 0 : 0, ++ onClick: open, ++ onKeyDown: open === void 0 ? void 0 : (event) => { ++ if (event.key === "Enter" || event.key === " ") open(); ++ } ++ }, ++ (0, import_react19.createElement)( ++ "span", ++ { className: "bt-project-title-wrap" }, ++ (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`) ++ ), ++ note ++ ); ++ }) ++ )); +} -+function faceMenuCss({ hidden, blank: blank3 }) { -+ if (hidden.length === 0) return ""; -+ const rows = hidden.map((row) => `[data-trigger-menu] [id="${optionId(row.source, row.index)}"]`); -+ const titles = hidden.filter((row) => row.title).map((row) => `[data-trigger-menu] [role="presentation"]:has(+ [id="${optionId(row.source, row.index)}"])`); -+ const css = [`${[...rows, ...titles].join(",")}{display:none!important}`]; -+ if (blank3) css.push("[data-trigger-menu]{display:none!important}"); -+ return css.join("\n"); ++var countText = (value) => value === null ? t("unknown") : String(value); ++function ProjectBoardRecord({ record }) { ++ const { decisions, meetings } = record; ++ const decisionRows = decisions.files.map((file) => (0, import_react19.createElement)( ++ "div", ++ { key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 }, ++ (0, import_react19.createElement)("span", { className: "bt-project-date" }, file.date), ++ (0, import_react19.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), ++ (0, import_react19.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) ++ )); ++ const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); ++ const proxyText = (meeting) => meeting.participants.filter((participant) => participant.speaksFor !== null).map((participant) => `${participant.name} ${t("for {user}", { user: participant.speaksFor })}`).join(", "); ++ const proxyRow = (meeting) => proxyText(meeting) === "" ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": meeting.participants.filter((p) => p.speaksFor !== null).map((p) => p.speaksFor).join(", ") }, proxyText(meeting)); ++ const levelOf = (meeting) => (0, import_react19.createElement)("span", { className: "bt-project-level", "data-level": meeting.level }, t(meeting.level)); ++ const decidedRows = meetings.decided.map((meeting) => (0, import_react19.createElement)( ++ "div", ++ { key: meeting.key, className: "bt-project-meeting", "data-status": "decided" }, ++ (0, import_react19.createElement)("span", { className: "bt-project-date" }, meeting.date), ++ (0, import_react19.createElement)("span", { className: "bt-project-title" }, meeting.question), ++ meeting.owner === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`), ++ proxyRow(meeting), ++ // A decided meeting whose file is gone still reads as decided; the missing path says so ++ // instead of the row disappearing with the file. ++ meeting.path === "" ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react19.createElement)("span", { className: "bt-project-path" }, meeting.path) ++ )); ++ const pendingRows = meetings.pending.map((meeting) => (0, import_react19.createElement)( ++ "div", ++ { key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status, "data-level": meeting.level }, ++ (0, import_react19.createElement)("span", { className: "bt-project-date" }, meeting.date), ++ (0, import_react19.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion), ++ levelOf(meeting), ++ recipientText(meeting.recipients) === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-recipients" }, recipientText(meeting.recipients)), ++ meeting.advance.allowed ? null : (0, import_react19.createElement)("span", { className: "bt-project-paused" }, meeting.advance.reason), ++ proxyRow(meeting), ++ (0, import_react19.createElement)("span", { className: "bt-project-status" }, meeting.status) ++ )); ++ return (0, import_react19.createElement)( ++ "section", ++ { className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) }, ++ (0, import_react19.createElement)("h4", null, t("Decisions")), ++ decisions.empty ? (0, import_react19.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, ++ (0, import_react19.createElement)("h4", null, t("Meetings")), ++ meetings.empty ? (0, import_react19.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows, ++ meetings.pending.length === 0 ? null : (0, import_react19.createElement)("h5", null, t("Not decided")), ++ meetings.pending.length === 0 ? null : pendingRows ++ ); +} -+function watchFaceMenu(controller, doc = globalThis.document) { -+ if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { -+ }; -+ const style = doc.createElement("style"); -+ style.setAttribute("data-bt-face-at", ""); -+ doc.head.appendChild(style); -+ let previous = null; -+ let stopped = false; -+ const later = typeof queueMicrotask === "function" ? queueMicrotask : (fn) => Promise.resolve().then(fn); -+ const apply2 = () => { -+ if (stopped) return; -+ const state = controller.menu.getSnapshot(); -+ const plan = planFaceMenu(state, previous); -+ const css = faceMenuCss(plan); -+ if (style.textContent !== css) style.textContent = css; -+ if (plan.dismiss) later(() => { -+ if (!stopped && planFaceMenu(controller.menu.getSnapshot(), previous).dismiss) controller.dismiss(); -+ }); -+ else if (plan.target) later(() => { -+ if (!stopped) controller.hover(plan.target.source, plan.target.index); -+ }); -+ else previous = state.open ? state.highlight : null; -+ }; -+ const unsubscribe = controller.menu.subscribe(apply2); -+ apply2(); -+ return () => { -+ stopped = true; -+ unsubscribe?.(); -+ style.remove(); ++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) { ++ const stored = project?.roles !== null && typeof project?.roles === "object" ? project.roles : {}; ++ const owner = []; ++ const addOwner = (id) => { ++ const clean = text(id); ++ if (clean !== "" && !owner.includes(clean)) owner.push(clean); + }; -+} -+ - // 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); ++ addOwner(project?.ownerUserId); ++ for (const id of idList(project?.ownerUserIds)) addOwner(id); ++ const maintainers = idList(stored.maintainers).filter((id) => !owner.includes(id)); ++ const members = idList(stored.members).filter((id) => !owner.includes(id) && !maintainers.includes(id)); ++ return { ++ chief: botIdOf(project?.chief), ++ agent: botIdOf(project?.assistant), ++ roles: { owner, maintainers, members }, ++ empty: owner.length === 0 && maintainers.length === 0 && members.length === 0, ++ // Both come from the `projects` listing (#1828), which the overlay keeps on the record: the ++ // `project-read` record answers no rank, and the panel never re-derives the host's policy. ++ role: text(project?.role), ++ choices: idList(project?.choices) }; - ascii(0, "RIFF"); - view.setUint32(4, 36 + samples.length * 2, true); -@@ -6030,9 +6030,9 @@ function waveBytes(samples, rate = 16e3) { - return bytes; +- requestAnimationFrame(look); } - 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() { -@@ -6121,233 +6121,9 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { - }; +-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") +- })); ++var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react19.createElement)( ++ "div", ++ { ++ key: role, ++ className: "bt-project-team-row", ++ "data-team-role": role, ++ // A row with no Bot is the fallback the slice specifies: still drawn, with nothing to open. ++ ...botId === "" ? { "data-team-state": "not-set" } : { ++ "data-open-bot": botId, ++ role: "button", ++ tabIndex: 0, ++ "aria-label": t("Open {name}'s chat", { name: botId }), ++ onClick: () => onOpenBot?.(botId), ++ onKeyDown: (event) => { ++ if (event.key === "Enter" || event.key === " ") { ++ event.preventDefault(); ++ onOpenBot?.(botId); ++ } ++ } ++ } ++ }, ++ (0, import_react19.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), ++ botId === "" ? (0, import_react19.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react19.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`), ++ // #1864: the dossier control sits inside the row, so the row still opens the chat (#1860) and a ++ // reader can look a Bot up first. A row with no Bot keeps the control: its panel reads "not set". ++ (0, import_react19.createElement)("button", { ++ key: "dossier", ++ type: "button", ++ className: "bt-project-team-dossier", ++ "data-dossier-role": role, ++ ...botId === "" ? {} : { "data-dossier-bot": botId }, ++ "aria-label": t("Open {name}'s dossier", { name: botId === "" ? label : botId }), ++ onClick: (event) => { ++ event?.stopPropagation(); ++ onOpenDossier?.({ role, label, botId }); ++ }, ++ onKeyDown: (event) => { ++ if (event.key === "Enter" || event.key === " ") { ++ event?.stopPropagation(); ++ onOpenDossier?.({ role, label, botId }); ++ } ++ } ++ }, t("Dossier")) ++); ++var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]]; ++function ProjectTeam({ project, onOpenBot, onOpenDossier }) { ++ const view = projectTeamView(project); ++ const poolId = (id) => (0, import_react19.createElement)("button", { ++ key: id, ++ type: "button", ++ className: "bt-project-team-dossier", ++ "data-dossier-role": "pool", ++ "data-dossier-bot": id, ++ "aria-label": t("Open {name}'s dossier", { name: id }), ++ onClick: (event) => { ++ event?.stopPropagation(); ++ onOpenDossier?.({ role: "pool", label: t("Role pool"), botId: id }); ++ } ++ }, `@${id}`); ++ const ranks = view.empty ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react19.createElement)( ++ "span", ++ { className: "bt-project-team-roles" }, ++ POOL_RANKS.map(([role, key]) => view.roles[key].length === 0 ? null : (0, import_react19.createElement)( ++ "span", ++ { key: role, className: "bt-project-team-role", "data-pool-role": role }, ++ `${roleText(role)}: `, ++ view.roles[key].map((id) => poolId(id)) ++ )) ++ ); ++ return (0, import_react19.createElement)( ++ "section", ++ { className: "bt-project-team", "data-team": "chief,agent,pool" }, ++ (0, import_react19.createElement)("h4", null, t("Project team")), ++ teamRow("chief", t("Project Chief"), view.chief, onOpenBot, onOpenDossier), ++ teamRow("agent", t("My agent"), view.agent, onOpenBot, onOpenDossier), ++ (0, import_react19.createElement)( ++ "div", ++ { key: "pool", className: "bt-project-team-row", "data-team-role": "pool" }, ++ (0, import_react19.createElement)("span", { key: "label", className: "bt-project-team-label" }, t("Role pool")), ++ ranks, ++ // The caller's own rank and what the host said they may switch to: the same answer the ++ // sidebar menu reads, printed where the pool is. ++ view.role === "" ? null : (0, import_react19.createElement)( ++ "span", ++ { key: "mine", className: "bt-project-team-myrole", "data-team-my-role": view.role }, ++ [ ++ t("My role"), ++ ": ", ++ roleText(view.role), ++ view.choices.length === 0 ? null : ` · ${t("Can switch to {roles}", { roles: view.choices.map(roleText).join(", ") })}` ++ ] ++ ) ++ ) ++ ); } - --// 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 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 () => { -- escapeLayers.splice(escapeLayers.indexOf(ref), 1); -- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); +- if (openShellSettings === openSettings) openShellSettings = null; - }; -- }, []); --} --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) +- }, [openSettings]); +- (0, import_react8.useEffect)(() => { +- if (!openOnboarding) return void 0; +- openShellOnboarding = openOnboarding; +- return () => { +- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; - }; -- }); -- 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(); -- }; -- 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 -- ); -- }) -- )) -- ) -- ) -- ); --} -- - // src/client/model-menu.js --var import_react13 = require("react"); --var import_react_dom6 = require("react-dom"); -+var import_react21 = 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_react21.useState)(void 0); -+ (0, import_react21.useEffect)(() => { - setFetched(void 0); - if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; - let live = true; -@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { - ]; - } - function EffortRows({ reasoning, current, onPick }) { -- return (0, import_react13.createElement)( -- import_react13.Fragment, -+ return (0, import_react21.createElement)( -+ import_react21.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)( -+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -+ effortRows(reasoning).map((row) => (0, import_react21.createElement)( - "button", - { - key: row.effort ?? "default", -@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { - "aria-checked": row.effort === current, - onClick: () => onPick(row.effort) - }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) -+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, row.label), -+ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react21.createElement)(CheckIcon) : null) - )) - ); - } - 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_react21.createElement)( -+ import_react21.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)( -+ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -+ list.map((model) => (0, import_react21.createElement)( - "button", - { - key: model.ref, -@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { - title: model.ref, - onClick: () => onPick(model.ref) - }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), -- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) -+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, model.name), -+ model.ref === roster.defaultModel ? (0, import_react21.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -+ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react21.createElement)(CheckIcon) : null) - )) - )); - } --var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); -+var Caret = () => (0, import_react21.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_react21.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); - function useAnchoredMenu() { -- const [place, setPlace] = (0, import_react13.useState)(null); -+ const [place, setPlace] = (0, import_react21.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 button = (0, import_react21.useRef)(null); -+ const menu = (0, import_react21.useRef)(null); - const close = () => setPlace(null); -- (0, import_react13.useEffect)(() => { -+ (0, import_react21.useEffect)(() => { - if (place === null) return void 0; - const onDown = (event) => { - if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { - return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; - } - function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { -+ if (bot?.remote !== void 0) return null; - const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); - const shown = modelOf(bot, roster); - const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6511,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo - else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); - }; - const models = roster.models ?? []; -- return (0, import_react13.createElement)( -- import_react13.Fragment, -+ return (0, import_react21.createElement)( -+ import_react21.Fragment, - null, -- (0, import_react13.createElement)( -+ (0, import_react21.createElement)( - "button", - { - ref: button, -@@ -6527,37 +6527,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo - "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, - onClick: toggle - }, -- (0, import_react13.createElement)( -+ (0, import_react21.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_react21.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -+ shown ? (0, import_react21.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_react21.createElement)(Caret) - ), -- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( -+ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react21.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: () => { -+ models.length > 0 ? (0, import_react21.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -+ effortRows(reasoning).length > 0 ? (0, import_react21.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react21.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_react21.createElement)(SlidersIcon), (0, import_react21.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_react21.useState)(""); -+ const [error, setError] = (0, import_react21.useState)(""); - const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; -- (0, import_react13.useEffect)(() => { -+ (0, import_react21.useEffect)(() => { - 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_react21.createElement)(RemoteGoneDock, { bot }) : null; - const models = roster.models ?? []; - const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; - if (bot.model && (!lost || models.length === 0)) return null; -@@ -6569,13 +6569,13 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { - void actions.refreshModels?.(); - }).finally(() => setBusy("")); - }; -- const 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_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" })] : [ -+ ...groupModels(models).map(([provider, list]) => (0, import_react21.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", { -+ (0, import_react21.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -+ (0, import_react21.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react21.createElement)("button", { - key: model.ref, - type: "button", - className: "bt-model-chip", -@@ -6584,50 +6584,115 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { - "aria-label": `${model.name} · ${provider}`, - title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, - onClick: () => pick2(model.ref) -- }, 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_react21.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_react21.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react21.createElement)(SlidersIcon), t("Manage models")) - ]; -- return (0, import_react13.createElement)( -+ return (0, import_react21.createElement)( - "div", - { className: "bt-model-dock" }, -- (0, import_react13.createElement)( -+ (0, import_react21.createElement)( - "div", - { className: "bt-astack", "data-lead": "" }, -- (0, import_react13.createElement)( -+ (0, import_react21.createElement)( - "div", - { className: "bt-msg" }, -- (0, import_react13.createElement)( -+ (0, import_react21.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_react21.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react21.createElement)("div", { className: "bt-bubble" }, text2) - ) - ) - ), -- (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 -+ (0, import_react21.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -+ error ? (0, import_react21.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : 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 dossierRowOf(team, botId) { ++ const id = text(botId); ++ if (id === "") return null; ++ const rows = Array.isArray(team?.rows) ? team.rows : []; ++ return rows.find((row) => row !== null && typeof row === "object" && text(row.botId) === id) ?? null; +} -+function RemoteGoneDock({ bot }) { -+ return (0, import_react21.createElement)( ++var dossierSection = (title, children) => (0, import_react19.createElement)( ++ "div", ++ { key: title, className: "bt-project-dossier-section" }, ++ (0, import_react19.createElement)("h4", null, title), ++ children ++); ++function ProjectDossierPanel({ row, label, onClose, onOpenBot } = {}) { ++ const known = row !== null && typeof row === "object"; ++ const botId = known ? text(row.botId) : ""; ++ const topics = known && Array.isArray(row.memory?.topics) ? row.memory.topics.map((name) => text(name)).filter((name) => name !== "") : []; ++ const skills = known && Array.isArray(row.skills) ? row.skills.map((name) => text(name)).filter((name) => name !== "") : []; ++ const meta = known ? [text(row.role), text(row.model)].filter((value) => value !== "") : []; ++ return (0, import_react19.createElement)( + "div", -+ { className: "bt-model-dock", "data-flybot": "remote-gone" }, -+ (0, import_react21.createElement)( ++ { className: "bt-project-panel bt-project-dossier", "data-panel": "project-dossier", "data-dossier-bot": botId === "" ? void 0 : botId }, ++ (0, import_react19.createElement)("h3", null, label === "" || label === void 0 ? t("Dossier") : `${label} · ${t("Dossier")}`), ++ known ? [ ++ dossierSection(t("Persona"), [ ++ meta.length === 0 ? null : (0, import_react19.createElement)("div", { key: "meta", className: "bt-project-dossier-meta" }, meta.map((value) => (0, import_react19.createElement)("span", { key: value }, value))), ++ (0, import_react19.createElement)("p", { key: "persona", className: "bt-project-dossier-text" }, text(row.persona) || t("Not set")) ++ ]), ++ dossierSection(t("Memory"), [ ++ (0, import_react19.createElement)("p", { key: "head", className: "bt-project-dossier-text" }, text(row.memory?.head) || t("No memory yet.")), ++ topics.length === 0 ? null : (0, import_react19.createElement)("div", { key: "topics", className: "bt-project-dossier-topics" }, topics.map((topic) => (0, import_react19.createElement)("span", { key: topic, className: "bt-project-dossier-topic" }, topic))) ++ ]), ++ dossierSection(t("Skills"), (0, import_react19.createElement)("p", { key: "skills", className: "bt-project-dossier-text" }, skills.length === 0 ? t("No skills attached.") : skills.join(", "))) ++ ] : (0, import_react19.createElement)("p", { className: "bt-project-missing" }, t("Not set")), ++ botId === "" || !known ? null : (0, import_react19.createElement)( + "div", -+ { className: "bt-astack", "data-lead": "" }, -+ (0, import_react21.createElement)( -+ "div", -+ { className: "bt-msg" }, -+ (0, import_react21.createElement)( -+ "div", -+ { className: "bt-msg-line" }, -+ (0, import_react21.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react21.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 })) -+ ) -+ ) -+ ) - ); - } - 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] -+ [t("Open settings to set up a custom model"), openHarnessModels] - ].filter(Boolean); - } -+var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; -+function KeyHelpLink() { -+ return (0, import_react21.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); -+} -+function missingModelKey(message) { -+ const text2 = String(message ?? ""); -+ if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; -+ return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; -+} -+function modelKeyChoices(actions, route) { -+ if (/^deepseek/i.test(route)) return setupChoices(actions); -+ return [[t("Manage models"), openHarnessModels]]; ++ { key: "actions", className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("button", { ++ type: "button", ++ className: "bt-soft bt-soft-sm", ++ "data-open-bot": botId, ++ onClick: () => onOpenBot?.(botId) ++ }, t("Open chat")) ++ ), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) ++ ) ++ ); +} -+function TurnErrorCell({ node, t: chatT, actions }) { -+ const data = node?.data ?? {}; -+ const code = typeof data.code === "string" && data.code ? data.code : void 0; -+ const raw = String(data.message ?? ""); -+ const shell = (key, fallback) => { -+ const text2 = typeof chatT === "function" ? chatT(key) : void 0; -+ return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; -+ }; -+ const missing = missingModelKey(raw); -+ const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); -+ const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; -+ const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; -+ const deepseek = missing ? /^deepseek/i.test(missing.route) : false; -+ return (0, import_react21.createElement)( ++function projectAgentFor(bot, selfId) { ++ if (bot === null || typeof bot !== "object" || bot.remote !== void 0) return null; ++ const projectId = text(bot.projectId); ++ if (projectId === "") return null; ++ const owner = text(bot.ownerId); ++ const self = text(selfId); ++ if (self !== "" && owner !== "" && owner !== self) return null; ++ return { projectId }; ++} ++function ProjectAgentEntry({ projectId, read, onOpen } = {}) { ++ const [agentId, setAgentId] = (0, import_react19.useState)(""); ++ const id = text(projectId); ++ const open = async () => { ++ let target = agentId; ++ if (target === "") { ++ try { ++ const record = await (read ?? call)("project-read", { id }); ++ 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_react19.createElement)("button", { ++ type: "button", ++ className: "bt-project-agent-entry", ++ "data-project-agent": id, ++ "data-open-bot": agentId === "" ? void 0 : agentId, ++ title: t("Open my agent"), ++ "aria-label": t("Open my agent"), ++ onClick: open ++ }, t("My agent")); ++} ++function ProjectBoardPane({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers, onOpenBot, onOpenDossier }) { ++ const view = projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics }); ++ return (0, import_react19.createElement)( + "div", -+ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, -+ (0, import_react21.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), -+ (0, import_react21.createElement)( ++ { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, ++ (0, import_react19.createElement)("h3", null, view.title), ++ (0, import_react19.createElement)(ProjectTeam, { project, onOpenBot, onOpenDossier }), ++ // Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown. ++ view.counts === null ? null : (0, import_react19.createElement)( + "div", -+ { className: "bt-turn-error-copy" }, -+ (0, import_react21.createElement)("span", { className: "bt-turn-error-title" }, title), -+ (0, import_react21.createElement)("span", { className: "bt-turn-error-message" }, message), -+ code ? (0, import_react21.createElement)("code", { className: "bt-turn-error-code" }, code) : null ++ { className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" }, ++ (0, import_react19.createElement)("span", null, [t("Waiting"), ": ", (0, import_react19.createElement)("b", { key: "w" }, countText(view.counts.waiting))]), ++ (0, import_react19.createElement)("span", null, [t("In flight"), ": ", (0, import_react19.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), ++ (0, import_react19.createElement)("span", null, [t("Blocked"), ": ", (0, import_react19.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), ++ (0, import_react19.createElement)("span", null, [t("Done"), ": ", (0, import_react19.createElement)("b", { key: "d" }, countText(view.counts.done))]) + ), -+ choices.length > 0 ? (0, import_react21.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_react19.createElement)( + "div", -+ { className: "bt-turn-error-actions" }, -+ choices.map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), -+ deepseek ? (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" }) : null -+ ) : null ++ { className: "bt-project-actions" }, ++ (0, import_react19.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), ++ (0, import_react19.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) + ), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ (0, import_react19.createElement)(ProjectMetrics, { view: view.metrics }), ++ (0, import_react19.createElement)(ProjectRunningTable, { view: view.metrics, onOpen }), ++ (0, import_react19.createElement)(ProjectBoardRows, { view, onOpen }), ++ (0, import_react19.createElement)(ProjectQueueGroups, { view, onOpen }), ++ (0, import_react19.createElement)(ProjectDoneGroup, { view, onOpen }), ++ (0, import_react19.createElement)(ProjectBoardRecord, { record: view.record }) + ); +} - 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 }); -+ return (0, import_react21.createElement)(SignInHost, { key: open, entry, actions }); - } - function SignInHost({ entry, actions }) { -- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); -+ const [ops] = (0, import_react21.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, { -+ const useHook = (source) => (selector) => (0, import_react21.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -+ const [hooks] = (0, import_react21.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -+ return (0, import_react21.createElement)(entry.component, { - ...ops, - ...hooks, - t: actions.accountT(), -@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) { - }); - } - 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} -+.bt-turn-error-copy{min-width:0;overflow-wrap:anywhere} -+.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135} -+.bt-turn-error-message{color:var(--bt-ink-2)} -+.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} -+.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px} -+.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px} -+.bt-model-help:hover{color:var(--bt-ink)} -+.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px} - .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} - .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} - .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} -@@ -6675,18 +6675,96 @@ var MODEL_CSS = ` - @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} - `; - -+// src/client/model-test.js -+var import_react22 = require("react"); -+var FAILURE_COPY = { -+ NO_MODEL: "Pick a model first.", -+ NO_KEY: "This model has no API key yet. Set one up in the model settings.", -+ AUTH: "The API key was rejected. Check it in the model settings.", -+ QUOTA: "The account is out of quota.", -+ RATE_LIMIT: "Too many requests right now. Try again in a moment.", -+ TIMEOUT: "The model did not answer within 30 seconds.", -+ NOT_SERVED: "This model is not available now. Check its provider in the model settings.", -+ MODEL_NOT_FOUND: "The provider does not know this model. Check the model name.", -+ TRANSPORT: "Could not reach the model service. Check the endpoint address and your network.", -+ SERVER: "The model service failed. Try again later.", -+ INVALID_REQUEST: "The model service refused the request. Check the endpoint and the model.", -+ UNKNOWN: "The test failed." -+}; -+function testOutcome(result, modelName) { -+ if (result?.ok === true) { -+ const seconds = typeof result.ms === "number" ? Math.max(0.1, Math.round(result.ms / 100) / 10) : void 0; -+ return { ok: true, text: seconds === void 0 ? t("Works. {model} answered.", { model: modelName }) : t("Works. {model} answered in {seconds} s.", { model: modelName, seconds }) }; -+ } -+ const code = Object.hasOwn(FAILURE_COPY, result?.code) ? result.code : "UNKNOWN"; -+ return { ok: false, code, text: t(FAILURE_COPY[code]) }; -+} -+var testModel = (id) => call("test-model", { id }); -+function ModelTestRow({ bot, modelName, test = testModel }) { -+ const [state, setState] = (0, import_react22.useState)({ busy: false, outcome: void 0 }); -+ const live = (0, import_react22.useRef)(true); -+ (0, import_react22.useEffect)(() => () => { -+ live.current = false; -+ }, []); -+ (0, import_react22.useEffect)(() => { -+ setState({ busy: false, outcome: void 0 }); -+ }, [bot.id, bot.model]); -+ const start = () => { -+ if (state.busy) return; -+ setState({ busy: true, outcome: void 0 }); -+ Promise.resolve().then(() => test(bot.id)).then( -+ (result) => testOutcome(result, modelName ?? result?.ref ?? ""), -+ (error) => ({ ok: false, text: String(error?.message ?? t("The test failed.")) }) -+ ).then((outcome2) => { -+ if (live.current) setState({ busy: false, outcome: outcome2 }); -+ }); ++function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { ++ const save = request ?? call; ++ const [kind, setKind] = (0, import_react19.useState)("bot"); ++ const [memberId, setMemberId] = (0, import_react19.useState)(""); ++ const [note, setNote] = (0, import_react19.useState)(""); ++ const noteLength = memoryLength(note); ++ const noteOver = noteLength > MEMORY_TEXT_MAX; ++ const noteSaveDisabled = text(note) === "" || noteOver; ++ const [assistantOff, setAssistantOff] = (0, import_react19.useState)(assistantMine === void 0 ? project?.assistant?.off === true : assistantMine?.off === true); ++ const [status, setStatus] = (0, import_react19.useState)(""); ++ const id = text(project?.id); ++ const run = async (endpoint, payload, done) => { ++ try { ++ const result = await save(endpoint, payload); ++ setStatus(done); ++ onSaved?.(result); ++ return result; ++ } catch (error) { ++ setStatus(hostText(error?.message ?? String(error))); ++ return null; ++ } + }; -+ const { busy, outcome } = state; -+ return (0, import_react22.createElement)( ++ return (0, import_react19.createElement)( + "div", -+ { className: "bt-set-row bt-bs-row bt-model-test" }, -+ (0, import_react22.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), -+ (0, import_react22.createElement)( -+ "span", -+ { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, -+ busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, -+ outcome?.code ? (0, import_react22.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null ++ { className: "bt-project-panel", "data-panel": "project-settings" }, ++ (0, import_react19.createElement)("h3", null, t("Project settings")), ++ (0, import_react19.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, ++ { className: "bt-project-field" }, ++ (0, import_react19.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)( ++ "select", ++ { "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) }, ++ (0, import_react19.createElement)("option", { value: "bot" }, t("Bot")), ++ (0, import_react19.createElement)("option", { value: "group" }, t("Group")) ++ ), ++ (0, import_react19.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind")) ++ ) ++ ), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-field" }, ++ (0, import_react19.createElement)("label", null, t("Project Assistant")), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("label", null, (0, import_react19.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save")) ++ ) ++ ), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-field" }, ++ // The host knows two memory kinds; the free text the panel takes is recorded as a decision, ++ // never as a third kind the host would refuse (#1820). ++ (0, import_react19.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")), ++ (0, import_react19.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }), ++ (0, import_react19.createElement)("div", { className: "bt-project-count", "data-role": "project-memory-count", "data-over": noteOver ? "true" : "false" }, `${noteLength}/${MEMORY_TEXT_MAX}`), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("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: () => { +- 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")) + ) + ), -+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ++ (0, import_react19.createElement)("div", { className: "bt-project-status-line", role: "status" }, status), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) ++ ) + ); +} -+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} -+.bt-model-test-result[data-ok=true]{color:#1a7f37} -+.bt-model-test-result[data-ok=false]{color:#c21d2e} -+.bt-model-test-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} -+/* #1347: the hint beside "Test connection" is prose and wraps; the button next to it keeps its own -+ words ("Test connection" on one line) and takes the room it needs. */ -+.bt-model-test>.bt-set-copy{flex:1 1 12ch;min-width:12ch} -+.bt-model-test>.bt-soft{flex:none;white-space:nowrap} -+@container bt-panel (max-width:300px){ -+ .bt-model-test{flex-wrap:wrap;row-gap:6px} -+ .bt-model-test>.bt-set-copy{flex:1 1 100%} -+} -+`; -+ - // src/client/folder-menu.js --var import_react14 = require("react"); --var import_react_dom7 = require("react-dom"); -+var import_react23 = require("react"); -+var import_react_dom9 = 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); -+var Tick = ({ on }) => (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react23.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 [info, setInfo] = (0, import_react23.useState)(null); -+ const [view, setView] = (0, import_react23.useState)("list"); -+ const [draft, setDraft] = (0, import_react23.useState)(""); -+ const [listing, setListing] = (0, import_react23.useState)(null); -+ const [problem, setProblem] = (0, import_react23.useState)(""); - const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); - const toggle = () => { - if (isOpen) { -@@ -6719,35 +6719,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } - }; - const known = info?.folders ?? []; - const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; -- const listView = (0, import_react14.createElement)( -+ const listView = (0, import_react23.createElement)( - "div", - { key: "list", className: "bt-folder-view" }, -- (0, import_react14.createElement)( -+ (0, import_react23.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 }) -+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, fallback), -+ (0, import_react23.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)( -+ folders.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -+ folders.map((folder) => (0, import_react23.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_react23.createElement)("span", { className: "bt-folder-copy" }, (0, import_react23.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react23.createElement)("span", { className: "bt-folder-path" }, folder.path)), -+ (0, import_react23.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: () => { -+ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ info?.picker === "native" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -+ info?.picker === "browse" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -+ (0, import_react23.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…"))) -+ } }, (0, import_react23.createElement)(PencilIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) - ); -- const typeView = (0, import_react14.createElement)( -+ const typeView = (0, import_react23.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", { -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react23.createElement)("input", { - className: "bt-folder-input", - autoFocus: true, - value: draft, -@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } - 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")) -+ (0, import_react23.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)( -+ const browseView = listing ? (0, import_react23.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)( -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -+ parent ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -+ inside.map((entry) => (0, import_react23.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) -+ (0, import_react23.createElement)(FolderIcon), -+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, entry.name), -+ (0, import_react23.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")) -+ inside.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -+ listing.truncated ? (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -+ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react23.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, -+ return (0, import_react23.createElement)( -+ import_react23.Fragment, - null, -- (0, import_react14.createElement)( -+ (0, import_react23.createElement)( - "button", - { - ref: button, -@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } - title: value, - onClick: toggle - }, -- (0, import_react14.createElement)( -+ (0, import_react23.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_react23.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -+ value ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, value) : null - ), -- (0, import_react14.createElement)(Caret) -+ (0, import_react23.createElement)(Caret) - ), -- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( -+ shownPlace ? (0, import_react_dom9.createPortal)((0, import_react23.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 -+ problem ? (0, import_react23.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null - ), document.body) : null - ); - } -@@ -6830,8 +6830,8 @@ var FOLDER_CSS = ` - `; - - // src/client/schedules.js --var import_react15 = require("react"); --var POLL_MS = 3e4; -+var import_react24 = require("react"); -+var POLL_MS4 = 3e4; - var shared = null; - var listeners = /* @__PURE__ */ new Set(); - function publish(view) { -@@ -6839,8 +6839,8 @@ function publish(view) { - for (const listener of listeners) listener(view); - } - function useSchedules(actions) { -- const [view, setView] = (0, import_react15.useState)(shared); -- (0, import_react15.useEffect)(() => { -+ const [view, setView] = (0, import_react24.useState)(shared); -+ (0, import_react24.useEffect)(() => { - listeners.add(setView); - let stopped = false; - let timer; -@@ -6848,7 +6848,7 @@ function useSchedules(actions) { - if (!stopped) publish(next); - }).catch(() => { - }).finally(() => { -- if (!stopped) timer = setTimeout(load, POLL_MS); -+ if (!stopped) timer = setTimeout(load, POLL_MS4); - }); - load(); - return () => { -@@ -6873,7 +6873,8 @@ function weekdayNames(days) { - function everyText(seconds) { - if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); - if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); -- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); -+ if (seconds % 60 === 0) return t("Every {count} minutes", { count: seconds / 60 }); -+ return t("Every {count} seconds", { count: seconds }); - } - function timingText(task) { - const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -@@ -6887,8 +6887,8 @@ function timingText(task) { - return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; - case "weekly": { - const days = (task.weekdays ?? []).join(); -- 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}`; -+ const text2 = 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 `${text2}${zone}`; - } - case "cron": - return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -@@ -6896,11 +6896,25 @@ function timingText(task) { - 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)(() => { -+var nextText = (task) => task.status === "paused" ? t("Paused") : task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); -+function lastRunText(run) { -+ if (run === void 0 || !Number.isFinite(run.at)) return ""; -+ const time = dayAndTime(run.at); -+ switch (run.result) { -+ case "running": -+ return t("Running since {time}", { time }); -+ case "stopped": -+ return t("Last run {time} · Stopped", { time }); -+ case "failed": -+ return t("Last run {time} · Failed: {reason}", { time, reason: hostText(run.reason || "Unknown error") }); -+ default: -+ return t("Last run {time}", { time }); -+ } -+} -+function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) { -+ const [confirming, setConfirming] = (0, import_react24.useState)(false); -+ const [busy, setBusy] = (0, import_react24.useState)(false); -+ (0, import_react24.useEffect)(() => { - if (!confirming) return void 0; - const timer = setTimeout(() => setConfirming(false), 3e3); - return () => clearTimeout(timer); -@@ -6913,20 +6913,49 @@ function TaskRow({ task, actions, onView, showBot, roster }) { - setBusy(true); - void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); - }; -+ const toggle = () => { -+ setBusy(true); -+ const step = task.status === "paused" ? actions.scheduleResume : actions.schedulePause; -+ void step(task.sessionId, task.id).then((next) => { -+ setBusy(false); -+ onView(next); -+ }, () => setBusy(false)); -+ }; -+ const [retryError, setRetryError] = (0, import_react24.useState)(""); -+ const retry = () => { -+ setBusy(true); -+ setRetryError(""); -+ void actions.scheduleRetry(task.sessionId, task.id).then((next) => { -+ setBusy(false); -+ onView(next); -+ }, (error) => { -+ setBusy(false); -+ setRetryError(hostText(error?.message ?? String(error))); -+ }); ++function ProjectMembersPanel({ project, rows, onClose } = {}) { ++ const owners = []; ++ const add = (id) => { ++ const clean = text(id); ++ if (clean !== "" && !owners.includes(clean)) owners.push(clean); + }; - const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; -- return (0, import_react15.createElement)( -+ const stoppable = task.status === "active" || task.status === "paused"; -+ const failed = task.lastRun?.result === "failed"; -+ const last = lastRunText(task.lastRun); -+ return (0, import_react24.createElement)( - "div", -- { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, -- showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -- (0, import_react15.createElement)( -+ { className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, -+ showBot ? (0, import_react24.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -+ (0, import_react24.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), -- task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null -+ (0, import_react24.createElement)("span", { className: "bt-task-title" }, task.title), -+ (0, import_react24.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_react24.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, -+ last ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, -+ retryError ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null - ), -- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), -- (0, import_react15.createElement)("button", { -+ (0, import_react24.createElement)("span", { className: "bt-task-next" }, nextText(task)), -+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, -+ onEdit ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react24.createElement)(PencilIcon)) : null, -+ onCopy ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react24.createElement)(DuplicateIcon)) : null, -+ stoppable ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, -+ (0, import_react24.createElement)("button", { - type: "button", - className: "bt-mini bt-task-remove", - "data-confirm": confirming || void 0, -@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { - "aria-label": confirming ? t("Click again to delete") : t("Delete task"), - title: confirming ? void 0 : t("Delete task"), - onClick: remove -- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) -+ }, confirming ? t("Delete") : (0, import_react24.createElement)(TrashIcon)) ++ add(project?.ownerUserId); ++ for (const id of Array.isArray(project?.ownerUserIds) ? project.ownerUserIds : []) add(id); ++ const boardMembers = []; ++ for (const row of Array.isArray(rows) ? rows : []) { ++ const name = text(row?.assignee); ++ if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name); ++ } ++ const chief = text(project?.chief?.botId); ++ return (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-panel", "data-panel": "project-members" }, ++ (0, import_react19.createElement)("h3", null, t("Members")), ++ (0, import_react19.createElement)( ++ "ul", ++ { className: "bt-project-members" }, ++ owners.map((owner) => (0, import_react19.createElement)( ++ "li", ++ { key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner }, ++ (0, import_react19.createElement)("span", null, `@${owner}`), ++ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("Owner")) ++ )), ++ chief === "" ? null : (0, import_react19.createElement)( ++ "li", ++ { key: `chief:${chief}`, className: "bt-project-member", "data-member-role": "chief", "data-member-id": chief }, ++ (0, import_react19.createElement)("span", null, t("Project Chief")), ++ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("Chief of this Project")) ++ ), ++ boardMembers.map((name) => (0, import_react19.createElement)( ++ "li", ++ { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, ++ (0, import_react19.createElement)("span", null, `@${name}`), ++ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) + )) +- ), document.body) : null ++ ), ++ owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, ++ (0, import_react19.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.")), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-row-actions" }, ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) ++ ) ); } - var pad = (number) => String(number).padStart(2, "0"); -@@ -6944,27 +6944,49 @@ function inAnHour() { - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; - } - var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; --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)(""); -+var UNITS = [["seconds", 1, "seconds"], ["minutes", 60, "minutes"], ["hours", 3600, "hours"], ["days", 86400, "days"]]; -+function localInput(ms) { -+ const date = new Date(ms); -+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; -+} -+function formStart(task) { -+ const seconds = task?.kind === "every" ? Number(task.everySeconds) : 3600; -+ const [unit, size] = seconds % 86400 === 0 ? ["days", 86400] : seconds % 3600 === 0 ? ["hours", 3600] : seconds % 60 === 0 ? ["minutes", 60] : ["seconds", 1]; -+ const once = task?.kind === "at" || task?.kind === "after"; +-function useLandOnMainBot(roster, useSessions, actions) { +- const mainView = useSessions((list) => { +- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); +- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; +- }); +- const lastBot = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!roster.ready || !roster.mainBotId) return void 0; +- const [id, blank3] = mainView.split("|"); +- const bot = id === "" ? void 0 : roster.byId[id]; +- if (bot !== void 0) { +- lastBot.current = { botId: bot.id, sessionId: id }; +- if (currentPart(bot) !== id) actions.landOn(bot.id); +- return void 0; +- } +- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { +- lastBot.current = null; ++async function readProjectBoard(read, wanted) { ++ const listing = await read("projects"); ++ const projects = Array.isArray(listing?.projects) ? listing.projects : []; ++ const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); ++ if (project === void 0) return { project: null, rows: [], done: [], canManage: false, tasks: [], team: { rows: [] } }; ++ const zone = typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function" ? void 0 : Intl.DateTimeFormat().resolvedOptions().timeZone; ++ const [record, board, taskList, team] = await Promise.all([ ++ read("project-read", { id: project.id }), ++ read("project-board", { id: project.id, timeZone: zone }), ++ read("tasks", {}), ++ read("project-team", { id: project.id }) ++ ]); ++ const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; ++ 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 { -+ title: task?.title ?? "", -+ prompt: task?.prompt ?? "", -+ kind: once ? "at" : task?.kind ?? "daily", -+ at: once && !Number.isNaN(Date.parse(task.scheduledAt)) ? localInput(Date.parse(task.scheduledAt)) : inAnHour(), -+ time: task?.time ? clock2(task.time) : "09:00", -+ weekdays: task?.kind === "weekly" && Array.isArray(task.weekdays) ? task.weekdays : [1, 2, 3, 4, 5], -+ count: Math.max(1, Math.round(seconds / size)), -+ unit, -+ timeZone: task?.timeZone || localZone() ++ project: withRole, ++ assistantMine: record?.assistantMine ?? null, ++ rows: board?.tasks ?? [], ++ done: board?.done ?? [], ++ canManage: record?.canManage === true, ++ tasks, ++ decisions: board?.decisions, ++ meetings: board?.meetings, ++ queues, ++ metrics: board?.metrics ?? null, ++ team: team !== null && typeof team === "object" ? team : { rows: [] } + }; +} -+function TaskForm({ roster, actions, start, task, onDone, onCancel }) { -+ const first = formStart(task); -+ const [target, setTarget] = (0, import_react24.useState)(task ? task.botId ?? task.roomId : start ?? ""); -+ const [title, setTitle] = (0, import_react24.useState)(first.title); -+ const [prompt, setPrompt] = (0, import_react24.useState)(first.prompt); -+ const [kind, setKind] = (0, import_react24.useState)(first.kind); -+ const [at, setAt] = (0, import_react24.useState)(first.at); -+ const [time, setTime] = (0, import_react24.useState)(first.time); -+ const [weekdays, setWeekdays] = (0, import_react24.useState)(first.weekdays); -+ const [count, setCount] = (0, import_react24.useState)(first.count); -+ const [unit, setUnit] = (0, import_react24.useState)(first.unit); -+ const [busy, setBusy] = (0, import_react24.useState)(false); -+ const [error, setError] = (0, import_react24.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 } : {} }; -+ if (kind === "cron") return { kind: "keep" }; -+ if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone }; -+ if (kind === "every") return { kind, seconds: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; -+ return { kind, time, timeZone: first.timeZone, ...kind === "weekly" ? { weekdays } : {} }; - }; - const submit = (event) => { - event.preventDefault(); -- if (busy) return; -+ if (busy || target === "") return; - setBusy(true); - setError(""); - let when3; -@@ -6975,151 +6975,298 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { - setBusy(false); - return; ++var PROJECT_BOARD_POLL_MS = 5e3; ++var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project"); ++function ProjectBoardOverlay({ actions, call: request } = {}) { ++ const read = request ?? call; ++ const [state, setState] = (0, import_react19.useState)(null); ++ const [panel, setPanel] = (0, import_react19.useState)(null); ++ const [dossier, setDossier] = (0, import_react19.useState)(null); ++ const [hash, setHash] = (0, import_react19.useState)(() => typeof window === "undefined" ? "" : window.location.hash); ++ const close = (0, import_react19.useCallback)(() => { ++ if (typeof window !== "undefined") window.location.hash = ""; ++ setPanel(null); ++ setState(null); ++ }, []); ++ (0, import_react19.useEffect)(() => { ++ if (typeof window === "undefined") return void 0; ++ const onKey = (event) => { ++ if (event.key !== "Escape") return; ++ if (panel !== null) setPanel(null); ++ else close(); ++ }; ++ window.addEventListener("keydown", onKey); ++ return () => window.removeEventListener("keydown", onKey); ++ }, [panel, close]); ++ (0, import_react19.useEffect)(() => { ++ if (typeof window === "undefined") return void 0; ++ const sync = () => setHash(window.location.hash); ++ window.addEventListener("hashchange", sync); ++ sync(); ++ return () => window.removeEventListener("hashchange", sync); ++ }, []); ++ (0, import_react19.useEffect)(() => { ++ if (typeof window === "undefined") return void 0; ++ if (!hash.startsWith("#project")) { ++ setState(null); + return void 0; } -- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { -+ const saved = task ? actions.scheduleUpdate({ sessionId: task.sessionId, id: task.id, target, title, prompt, when: when3 }) : actions.scheduleCreate({ target, title, prompt, when: when3 }); -+ void saved.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)( -+ return (0, import_react24.createElement)( - "form", - { className: "bt-set-card bt-task-form", onSubmit: submit }, -- (0, import_react15.createElement)( -+ (0, import_react24.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)( -+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -+ (0, import_react24.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_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -+ (0, import_react24.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react24.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -+ roster.rooms.length > 0 ? (0, import_react24.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react24.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null - ) - ), -- (0, import_react15.createElement)( -+ (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react24.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)( -+ (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -+ (0, import_react24.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) - ), -- (0, import_react15.createElement)( -+ (0, import_react24.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)( -+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("When"))), -+ (0, import_react24.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -+ (0, import_react24.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", { -+ kind === "cron" ? (0, import_react24.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, -+ kind === "at" ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react24.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, -+ kind === "daily" || kind === "weekly" ? (0, import_react24.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_react24.createElement)( -+ import_react24.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)( -+ (0, import_react24.createElement)("span", { className: "bt-task-every" }, t("Every")), -+ (0, import_react24.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_react24.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))) -+ UNITS.map(([id, , label]) => (0, import_react24.createElement)("option", { key: id, value: id }, t(label))) - ) - ) : 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)( -+ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ (0, import_react24.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")) -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react24.createElement)( -+ "button", -+ { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, -+ task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") +- 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 ++ const wanted = hash.slice("#project".length).replace(/^=/, ""); ++ let gone = false; ++ const loadBoard = () => readProjectBoard(read, wanted); ++ const refresh = async (first) => { ++ try { ++ const next = await loadBoard(); ++ if (!gone) setState(next); ++ } catch { ++ if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); ++ } ++ }; ++ void refresh(true); ++ const timer = setInterval(() => { ++ if (!boardPollPaused()) void refresh(false); ++ }, PROJECT_BOARD_POLL_MS); ++ return () => { ++ gone = true; ++ clearInterval(timer); ++ }; ++ }, [hash]); ++ if (state === null) return null; ++ return (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-overlay" }, ++ (0, import_react19.createElement)("div", { className: "bt-scrim", onClick: close }), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") }, ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-mem-head" }, ++ (0, import_react19.createElement)("h2", {}, t("Project board")), ++ (0, import_react19.createElement)("span", { className: "bt-mem-head-gap" }), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close")) ++ ), ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-body" }, ++ state.project === null ? (0, import_react19.createElement)( ++ "div", ++ { className: "bt-project-empty" }, ++ (0, import_react19.createElement)("p", null, t("No Project yet — create one.")), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { ++ close(); ++ actions?.openSettings?.("projects"); ++ } }, t("Open Projects in Settings")) ++ ) : panel === "dossier" ? (0, import_react19.createElement)(ProjectDossierPanel, { ++ row: dossierRowOf(state.team, dossier?.botId), ++ label: dossier?.label ?? "", ++ onClose: () => setPanel(null), ++ onOpenBot: (id) => { ++ close(); ++ actions?.openSession?.(id); ++ } ++ }) : panel === "settings" ? (0, import_react19.createElement)(ProjectSettingsPanel, { project: state.project, assistantMine: state.assistantMine, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react19.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react19.createElement)(ProjectBoardPane, { ++ project: state.project, ++ rows: state.rows, ++ done: state.done, ++ canManage: state.canManage, ++ tasks: state.tasks, ++ decisions: state.decisions, ++ meetings: state.meetings, ++ queues: state.queues, ++ metrics: state.metrics, ++ team: state.team, ++ onSettings: () => setPanel("settings"), ++ onMembers: () => setPanel("members"), ++ // #1864: a team row's Dossier control opens the read-only panel, fed by the same read. ++ onOpenDossier: (value) => { ++ setDossier(value); ++ setPanel("dossier"); ++ }, ++ // #1860: a team row opens that Bot's chat. The board is a modal over the conversation, ++ // so it closes first — otherwise the chat opens behind it. ++ onOpenBot: (id) => { ++ close(); ++ actions?.openSession?.(id); ++ }, ++ // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and ++ // lands on that Bot or group. Nothing here re-decides which board that is. ++ onOpen: (board) => { ++ if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`; ++ actions?.openBoard?.(); ++ setState(null); ++ } ++ }) + ) - ) ++ ) ); } --function AutomationOff() { -- return (0, import_react15.createElement)( -+function openPlugins(actions) { -+ actions.closeSettings(); -+ actions.selectPanel("plugins"); -+} -+function AutomationOff({ actions }) { -+ return (0, import_react24.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")) -+ (0, import_react24.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react24.createElement)(ClockIcon)), -+ (0, import_react24.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) -+ ); -+} -+function QuietHoursCard({ view, actions }) { -+ const quiet = view.quietHours ?? { on: false }; -+ const [busy, setBusy] = (0, import_react24.useState)(false); -+ const [error, setError] = (0, import_react24.useState)(""); -+ const [from, setFrom] = (0, import_react24.useState)(quiet.from ?? "22:00"); -+ const [to, setTo] = (0, import_react24.useState)(quiet.to ?? "07:00"); -+ const unchecked = view.quietUnchecked === true; -+ const refresh = () => { -+ void Promise.resolve(actions.schedules?.()).then((next) => { -+ if (next) publish(next); -+ }).catch(() => { -+ }); -+ }; -+ const save = (next) => { -+ if (busy) return; -+ setBusy(true); -+ setError(""); -+ const shown = next.on ? { on: true, from: next.from ?? from, to: next.to ?? to } : { on: false }; -+ void Promise.resolve(actions.setPrefs({ quietHours: next })).then(() => { -+ publish({ ...view, quietHours: shown }); -+ refresh(); -+ }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); -+ }; -+ return (0, import_react24.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react24.createElement)("h3", null, t("Quiet hours")), -+ (0, import_react24.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ (0, import_react24.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react24.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react24.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), -+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) -+ ), -+ (0, import_react24.createElement)("button", { -+ type: "button", -+ className: "bt-soft bt-soft-sm", -+ disabled: busy, -+ "aria-pressed": quiet.on, -+ onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) -+ }, quiet.on ? t("Turn off") : t("Turn on")) -+ ), -+ quiet.on ? (0, import_react24.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Window"))), -+ (0, import_react24.createElement)( -+ "div", -+ { className: "bt-task-when" }, -+ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), -+ (0, import_react24.createElement)("span", { className: "bt-task-every" }, "–"), -+ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) -+ ) -+ ) : null, -+ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ quiet.on && unchecked ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null -+ ) - ); - } - 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 [adding, setAdding] = (0, import_react24.useState)(Boolean(start)); -+ const [editing, setEditing] = (0, import_react24.useState)(null); -+ if (view === null) return (0, import_react24.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -+ if (!view.available) return (0, import_react24.createElement)(AutomationOff, { actions }); - const shown = (next) => { - publish(next); - setAdding(false); -+ setEditing(null); -+ }; -+ const edit = (task) => { -+ setAdding(false); -+ setEditing(task); - }; - 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)]) -+ ...roster.bots.map((bot) => [bot.id, (0, import_react24.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_react24.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)( -+ (0, import_react24.createElement)(QuietHoursCard, { key: "quiet", view, actions }), -+ adding ? (0, import_react24.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react24.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { -+ setEditing(null); -+ setAdding(true); -+ } }, (0, import_react24.createElement)(PlusIcon), t("New task"))), -+ byChat.length === 0 && !adding ? (0, import_react24.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react24.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 }))) -+ (0, import_react24.createElement)("h3", { className: "bt-task-head" }, avatar, name), -+ (0, import_react24.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) - )) - ]; +-var RETURN_CSS = ` +-.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} +-.bt-return:hover{background:rgba(127,127,127,.14)} +-.bt-return:active{transform:scale(.96)} +-.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} +-.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} +-/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ +-.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} +-.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-`; +-function BrandMark({ size = 24 }) { +- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); +-} +-function BrandName() { +- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); +-} +-var SIDEBAR_CSS = ` +-.bt-side-top .bt-brand{cursor:default;padding-left:6px} +-.bt-side-top .bt-brand:hover{background:none} +-.bt-brand .bt-mark{flex:none} +-.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} +-.bt-rail .bt-row{position:relative} +-.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} +-.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +-[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} +-.bt-foot-account{order:-1;flex:none;display:inline-flex} +-[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} +- +-/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a +- loop while the install runs. */ +-.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-round.bt-update svg{width:17px;height:17px} +-.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} +-@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} +-.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} +-@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} +-.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-.bt-update-title{font-size:14px;line-height:20px;font-weight:600} +-.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} +-.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} +-.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} +-.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} +-.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} +-.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} +-.bt-update-go:hover:not(:disabled){opacity:.9} +-.bt-update-go:active:not(:disabled){transform:scale(.96)} +-.bt-update-go:disabled{opacity:.6;cursor:default} +-.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} +-@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} +- +-/* The DSH Agent row and its expanding Session list. The black whale is the +- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ +-.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} +-@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} +-.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-agent-sub[data-open]{grid-template-rows:1fr} +-.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} +-.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} +-.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} +-.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} +-.bt-agent-new{color:var(--bt-ink-3)} +-.bt-agent-new svg{width:14px;height:14px;flex:none} +-.bt-agent-edit{padding:3px 10px 3px 4px} +-.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} +-.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} +-@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} +-@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} +-`; + + // src/client/header.js +-var import_react9 = require("react"); +-var import_react_dom4 = require("react-dom"); +-var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); ++var import_react20 = require("react"); ++var import_react_dom6 = require("react-dom"); ++var GoArrow = () => (0, import_react20.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_react20.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 [shown, setShown] = (0, import_react20.useState)(false); ++ (0, import_react20.useEffect)(() => { + const left = seen + MEMORY_NOTICE_MS - Date.now(); + setShown(seen > 0 && left > 0); + if (seen === 0 || left <= 0) return void 0; +@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { + return shown; } -+function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { -+ const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; -+ const [copying, setCopying] = (0, import_react24.useState)(null); -+ const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0; -+ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react24.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 })); -+ return (0, import_react24.createElement)( -+ import_react24.Fragment, -+ null, -+ open ? (0, import_react24.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, -+ source ? (0, import_react24.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => { -+ setCopying(null); -+ onDone(next); -+ }, onCancel: () => setCopying(null) }) : null, -+ rows.length + extra.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null -+ ); -+} -+function CopyRow({ task, roster, actions, onView, onCancel }) { -+ const others = [ -+ ...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]), -+ ...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name]) -+ ]; -+ const [target, setTarget] = (0, import_react24.useState)(""); -+ const [busy, setBusy] = (0, import_react24.useState)(false); -+ const [error, setError] = (0, import_react24.useState)(""); -+ const submit = (event) => { -+ event.preventDefault(); -+ if (busy || target === "") return; -+ setBusy(true); -+ setError(""); -+ void actions.scheduleCreate({ target, copyFrom: { sessionId: task.sessionId, id: task.id } }).then(onView, (failure) => { -+ setBusy(false); -+ setError(hostText(failure?.message ?? String(failure))); -+ }); -+ }; -+ return (0, import_react24.createElement)( -+ "form", -+ { className: "bt-set-row bt-task-copy", onSubmit: submit }, -+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))), -+ (0, import_react24.createElement)( -+ "select", -+ { className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) }, -+ (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -+ others.map(([id, name]) => (0, import_react24.createElement)("option", { key: id, value: id }, name)) -+ ), -+ (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")), -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ error ? (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null -+ ); -+} - function BotSchedules({ bot, roster, actions }) { - const view = useSchedules(actions); -+ const [editing, setEditing] = (0, import_react24.useState)(null); - if (view === null) return null; - const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; -- return (0, import_react15.createElement)( -+ return (0, import_react24.createElement)( - "section", - { className: "bt-set-section" }, -- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), -- (0, import_react15.createElement)( -+ (0, import_react24.createElement)("h3", null, t("Scheduled tasks")), -+ !view.available ? (0, import_react24.createElement)( - "div", - { className: "bt-set-card" }, -- !view.available ? (0, import_react15.createElement)( -+ (0, import_react24.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")) -- ] -- ) -+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) -+ ) -+ ) : (0, import_react24.createElement)(TaskList, { -+ tasks, -+ editing, -+ roster, -+ actions, -+ onEdit: setEditing, -+ onDone: (next) => { -+ publish(next); -+ setEditing(null); -+ }, -+ onCancel: () => setEditing(null), -+ extra: [(0, import_react24.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react24.createElement)(PlusIcon), t("New task"))] -+ }) + function MemoryNotice({ shown }) { +- const inner = (0, import_react9.useRef)(null); +- const [width, setWidth] = (0, import_react9.useState)(0); ++ const inner = (0, import_react20.useRef)(null); ++ const [width, setWidth] = (0, import_react20.useState)(0); + const label = t("Memory updated"); +- (0, import_react9.useLayoutEffect)(() => { ++ (0, import_react20.useLayoutEffect)(() => { + if (shown) setWidth(inner.current?.scrollWidth ?? 0); + }, [label, shown]); +- return (0, import_react9.createElement)( ++ return (0, import_react20.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) ++ (0, import_react20.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react20.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react20.createElement)(MemoryIcon)), label) ); } - 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)( -+ return (0, import_react24.createElement)( +-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) => { +@@ -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_react20.useEffect)(() => { + if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); + else document.body.style.removeProperty("--bt-first-sep"); + return () => document.body.style.removeProperty("--bt-first-sep"); +@@ -5546,54 +5546,49 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe + const owner = roster.byId[sessionId]?.id ?? sessionId; + const doing = useActivity((map) => map[owner]); + const bot = roster.byId[sessionId]; ++ const projectAgent = projectAgentFor(bot, roster.self?.memberId); + const room2 = roster.roomsById[sessionId]; + const detailsOpen = useUi((state) => state.details === sessionId); +- const name = bot?.name ?? room2?.name ?? ""; ++ const full2 = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; ++ const face = room2?.origin === "face-to-face"; ++ const name = face ? faceRowTitle(full2) : full2; + const useNews = useMemoryNews ?? noNews; + const memoryIds = bot ? [bot.id] : room2?.members ?? []; + const notice = useMemoryNotice(memoryIds, useNews); +- const changed = useNews((map) => latestMemoryChange(map, memoryIds)); +- (0, import_react9.useEffect)(() => { +- document.body.style.setProperty("--bt-placeholder", JSON.stringify(name ? t("Message {name}", { name }) : t("Message"))); +- }, [name, t("Message")]); +- (0, import_react9.useEffect)(() => { +- actions.beginVisit(sessionId); +- return () => actions.endVisit(sessionId); +- }, [sessionId]); +- (0, import_react9.useEffect)(() => { +- if (room2) document.body.dataset.btRoom = ""; +- else delete document.body.dataset.btRoom; +- return () => { +- delete document.body.dataset.btRoom; +- }; +- }, [Boolean(room2)]); +- const memoryNotice = notice && changed !== null && roster.memory !== false; +- return (0, import_react9.createElement)( ++ const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); ++ const memoryNotice = notice && changed2 !== null && roster.memory !== false; ++ return (0, import_react20.createElement)( "div", - null, -- (0, import_react15.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), -- tasks.length === 0 ? (0, import_react15.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 in Harness settings.") : t("No scheduled tasks yet. Add one")) : (0, import_react15.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react15.createElement)( -+ (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), -+ tasks.length === 0 ? (0, import_react24.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.") : t("No scheduled tasks yet. Add one")) : (0, import_react24.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react24.createElement)( + { className: "bt-head" }, +- (0, import_react9.createElement)( ++ (0, import_react20.createElement)( "button", - { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, -- (0, import_react15.createElement)(ClockIcon), -- (0, import_react15.createElement)("span", { className: "bt-task-line-title" }, task.title), -- (0, import_react15.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) -+ (0, import_react24.createElement)(ClockIcon), -+ (0, import_react24.createElement)("span", { className: "bt-task-line-title" }, task.title), -+ (0, import_react24.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) - ))) + { + 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_react20.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react20.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, ++ (0, import_react20.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_react20.createElement)(MemoryNotice, { shown: memoryNotice }), ++ (0, import_react20.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react20.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_react20.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), ++ projectAgent === null ? null : (0, import_react20.createElement)(ProjectAgentEntry, { ++ projectId: projectAgent.projectId, ++ read: call, ++ onOpen: (id) => actions?.openSession?.(id) ++ }), ++ (0, import_react20.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), ++ (0, import_react20.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), ++ (0, import_react20.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) ); } - var SCHEDULE_CSS = ` - .bt-task{gap:10px} -+.bt-task-copy{flex-wrap:wrap;gap:8px} -+.bt-task-copy .bt-task-bot{flex:1;min-width:140px;max-width:100%} - .bt-task[data-done]{opacity:.6} - .bt-task[data-busy]{opacity:.4} - .bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} -@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = ` - .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} - .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} - .bt-task-remove svg{width:14px;height:14px} -+.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} -+.bt-task-tool svg{width:14px;height:14px} -+.bt-task-pause{height:26px;color:var(--bt-ink-2)} -+.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere} -+.bt-task-last[data-failed]{color:#c21d2e} -+.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)} - .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} - .bt-task-head{display:flex;align-items:center;gap:6px} - .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -@@ -7158,12 +7158,12 @@ var SCHEDULE_CSS = ` - `; - - // src/client/capability-settings.js --var import_react16 = require("react"); -+var import_react25 = require("react"); - var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); --var parseMap = (text, label) => { -+var parseMap = (text2, label) => { - let value; - try { -- value = JSON.parse(text || "{}"); -+ value = JSON.parse(text2 || "{}"); - } catch { - throw new Error(t("{label} must be valid JSON.", { label })); - } -@@ -7172,11 +7172,11 @@ var parseMap = (text, label) => { - }; - var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); - function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { -- return (0, import_react16.createElement)( -+ return (0, import_react25.createElement)( - "label", - { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t(label))), -- (0, import_react16.createElement)(multiline ? "textarea" : "input", { -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t(label))), -+ (0, import_react25.createElement)(multiline ? "textarea" : "input", { - className: multiline ? "bt-input bt-input-area" : "bt-input", - value, - disabled, -@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl - ); + function useTranscriptView() { +- const [view, setView] = (0, import_react9.useState)(null); +- (0, import_react9.useEffect)(() => { ++ const [view, setView] = (0, import_react20.useState)(null); ++ (0, import_react20.useEffect)(() => { + let frame = 0; + let watched = null; + const observer = new MutationObserver(() => schedule()); +@@ -5642,7 +5642,7 @@ function useTranscriptView() { + }, []); + return view; } - function CapabilityForm({ draft, setDraft, items, actions, onClose }) { -- const [error, setError] = (0, import_react16.useState)(""); -+ const [error, setError] = (0, import_react25.useState)(""); - const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); - const submit = async (event) => { - event.preventDefault(); -@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { - if (draft.kind === "mcp" && draft.transport === "stdio") { - payload.transport = "stdio"; - payload.command = draft.command; -- payload.args = draft.argsText.split("\n").filter((line) => line !== ""); -+ payload.args = draft.argsText.split("\n").filter((line2) => line2 !== ""); - payload.env = parseMap(draft.envText, t("Environment")); - } else if (draft.kind === "mcp") { - payload.transport = "streamable-http"; -@@ -7219,95 +7219,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { - } - }; - const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); -- return (0, import_react16.createElement)( -+ return (0, import_react25.createElement)( - "form", - { className: "bt-connector-form", onSubmit: submit }, -- (0, import_react16.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), -+ (0, import_react25.createElement)( - "label", - { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Type"))), -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Type"))), -+ (0, import_react25.createElement)( - "select", - { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, -- ["mcp", "skill", "pack"].map((kind) => (0, import_react16.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) -+ ["mcp", "skill", "pack"].map((kind) => (0, import_react25.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) - ) +-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; +@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + }); + 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_react20.useState)({ base: null, dismissed: false }); ++ const [above, setAbove] = (0, import_react20.useState)({ seen: false, dismissed: false }); ++ (0, import_react20.useEffect)(() => { + setRelease({ base: unpinned ? replies : null, dismissed: false }); + }, [unpinned]); +- (0, import_react9.useEffect)(() => { ++ (0, import_react20.useEffect)(() => { + if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); + }, [view?.divider]); ++ const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); ++ (0, import_react20.useEffect)(() => { ++ if (above.seen && movedOn) actions?.settleVisit?.(sessionId); ++ }, [above.seen, movedOn, sessionId]); + const fresh = release.base === null ? 0 : replies - release.base; + const showDown = unpinned && fresh > 0 && !release.dismissed; + const showUp = view?.divider === "above" && !above.seen && !above.dismissed; +- (0, import_react9.useEffect)(() => { ++ (0, import_react20.useEffect)(() => { + const scroller = document.querySelector("[data-conversation-scroll]"); + if (!scroller) return void 0; + scroller.toggleAttribute("data-bt-news", showDown); +@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + }, [showDown]); + if (!view || !showDown && !showUp) return null; + const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); +- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( ++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react20.createElement)( + "div", + { className: "bt-news", "data-direction": direction, style }, +- (0, import_react9.createElement)( ++ (0, import_react20.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)), ++ (0, import_react20.createElement)("span", { className: "bt-news-ico" }, (0, import_react20.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), + label(count) ), -- (0, import_react16.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), -- draft.kind === "mcp" ? (0, import_react16.createElement)( -+ (0, import_react25.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), -+ draft.kind === "mcp" ? (0, import_react25.createElement)( - "label", - { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), -+ (0, import_react25.createElement)( - "select", - { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, -- (0, import_react16.createElement)("option", { value: "stdio" }, t("Local command")), -- (0, import_react16.createElement)("option", { value: "streamable-http" }, t("HTTP server")) -+ (0, import_react25.createElement)("option", { value: "stdio" }, t("Local command")), -+ (0, import_react25.createElement)("option", { value: "streamable-http" }, t("HTTP server")) - ) - ) : null, -- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react16.createElement)( -+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react25.createElement)( - "div", - null, -- (0, import_react16.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), -- (0, import_react16.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), -- (0, import_react16.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ (0, import_react25.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), -+ (0, import_react25.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), -+ (0, import_react25.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) - ) : null, -- draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react16.createElement)( -+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react25.createElement)( - "div", +- (0, import_react9.createElement)("button", { ++ (0, import_react20.createElement)("button", { + type: "button", + className: "bt-news-x", + "aria-label": t("Dismiss new messages"), +@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + event.stopPropagation(); + onDismiss(); + } +- }, (0, import_react9.createElement)(CloseIcon)) ++ }, (0, import_react20.createElement)(CloseIcon)) + ); + const toLatest = () => { + const scroller = document.querySelector("[data-conversation-scroll]"); +@@ -5703,9 +5703,9 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); + }; + const 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_react20.createElement)( ++ import_react20.Fragment, null, -- (0, import_react16.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), -- (0, import_react16.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ (0, import_react25.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), -+ (0, import_react25.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) - ) : null, -- draft.kind === "skill" ? (0, import_react16.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, -- draft.kind === "pack" ? (0, import_react16.createElement)( -+ draft.kind === "skill" ? (0, import_react25.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, -+ draft.kind === "pack" ? (0, import_react25.createElement)( - "div", - { className: "bt-set-card" }, -- available.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react16.createElement)( -+ available.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react25.createElement)( - "label", - { key: item.name, className: "bt-set-row bt-bs-row" }, -- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, item.name)), -- (0, import_react16.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, item.name)), -+ (0, import_react25.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) - )) - ) : null, -- error ? (0, import_react16.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, -- (0, import_react16.createElement)( -+ error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, -+ (0, import_react25.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), -- (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), -+ (0, import_react25.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) - ) - ); - } - function CapabilityRow({ item, onEdit, onDelete, onApprove, confirming }) { - const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); - const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; -- return (0, import_react16.createElement)( -+ return (0, import_react25.createElement)( - "div", - { className: "bt-connector" }, -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)( - "div", - { className: "bt-connector-head" }, -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)( - "span", - { className: "bt-connector-text" }, -- (0, import_react16.createElement)("span", { className: "bt-connector-name" }, item.name), -- (0, import_react16.createElement)("span", { className: "bt-connector-status" }, secondary) -+ (0, import_react25.createElement)("span", { className: "bt-connector-name" }, item.name), -+ (0, import_react25.createElement)("span", { className: "bt-connector-status" }, secondary) - ), -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)( - "span", - { className: "bt-connector-actions" }, -- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, -- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), -- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) -+ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) - ) - ) - ); - } - function CapabilitiesPage({ roster, actions, run }) { - const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name)); -- const [draft, setDraft] = (0, import_react16.useState)(null); -- const [confirming, setConfirming] = (0, import_react16.useState)(""); -- (0, import_react16.useEffect)(() => { -+ const [draft, setDraft] = (0, import_react25.useState)(null); -+ const [confirming, setConfirming] = (0, import_react25.useState)(""); -+ (0, import_react25.useEffect)(() => { - if (!confirming) return void 0; - const timer = setTimeout(() => setConfirming(""), 4e3); - return () => clearTimeout(timer); -@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) { - setConfirming(""); - void run(actions.deleteCapability(item.name)); - }; -- return (0, import_react16.createElement)( -+ return (0, import_react25.createElement)( - "section", - { className: "bt-set-section" }, -- (0, import_react16.createElement)("h3", null, t("Shared capabilities")), -- (0, import_react16.createElement)( -+ (0, import_react25.createElement)("h3", null, t("Shared capabilities")), -+ (0, import_react25.createElement)( - "div", - { className: "bt-actions" }, -- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) - ), -- draft ? (0, import_react16.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, -- (0, import_react16.createElement)( -+ draft ? (0, import_react25.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, -+ (0, import_react25.createElement)( - "div", - { className: "bt-set-card bt-connectors-card" }, -- items.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react16.createElement)(CapabilityRow, { -+ items.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react25.createElement)(CapabilityRow, { - key: item.name, - item, - confirming: confirming === item.name, -@@ -7359,10 +7359,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow - const attached = new Set(bot.capabilities ?? []); - const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; - if (items.length === 0) return null; -- return (0, import_react16.createElement)( -+ return (0, import_react25.createElement)( - Card2, - { title: t("Capabilities") }, -- items.map((item) => (0, import_react16.createElement)(SwitchRow2, { -+ items.map((item) => (0, import_react25.createElement)(SwitchRow2, { - key: item.name, - label: item.name, - hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7372,9 +7372,55 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow + 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/connector-bots.js -+var import_react26 = require("react"); -+function ownBots(roster) { -+ const me = roster?.self?.memberId; -+ return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); -+} -+var connectedConnectors = (roster) => (roster?.connectors ?? []).filter((connector) => connector.state !== "off" && connector.kind !== "sender" && !(connector.kind === "channel" && connector.mode !== "app")); -+var isOn = (bot, id) => bot.connectors?.[id] !== false; -+function ownHint(bot, id) { -+ return isOn(bot, id) ? t("On. This Bot can use it on every turn.") : t("Off. This Bot will not use it."); -+} -+function usageText(roster, id) { -+ const bots = ownBots(roster); -+ const own = bots.filter((bot) => isOn(bot, id)).length; -+ return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); -+} -+function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { -+ return (0, import_react26.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); -+} -+function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { -+ const bots = ownBots(roster); -+ return (0, import_react26.createElement)( -+ "div", -+ { className: "bt-connector-bots" }, -+ bots.length === 0 ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react26.createElement)( -+ "div", -+ { key: bot.id, className: "bt-connector-bot" }, -+ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) -+ )), -+ (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) -+ ); ++// src/client/composer-label.js ++var ATTRIBUTES = ["aria-label", "data-placeholder"]; ++function holdComposerLabel(doc, label) { ++ doc.body.style.setProperty("--bt-placeholder", JSON.stringify(label)); ++ const shell = /* @__PURE__ */ new Map(); ++ const apply2 = () => { ++ for (const el of doc.querySelectorAll("[data-composer-input]")) { ++ for (const name of ATTRIBUTES) { ++ const current = el.getAttribute(name); ++ if (current === label) continue; ++ if (!shell.has(el)) shell.set(el, {}); ++ shell.get(el)[name] = current; ++ el.setAttribute(name, label); ++ } ++ } ++ }; ++ apply2(); ++ const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver; ++ const watcher = typeof Observer === "function" ? new Observer(apply2) : null; ++ watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES }); ++ return () => { ++ watcher?.disconnect(); ++ for (const [el, values] of shell) { ++ if (!el.isConnected) continue; ++ for (const [name, value] of Object.entries(values)) { ++ if (value == null) el.removeAttribute(name); ++ else el.setAttribute(name, value); ++ } ++ } ++ }; +} -+function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { -+ const connected = connectedConnectors(roster); -+ if (connected.length === 0) return null; -+ return (0, import_react26.createElement)( -+ Card2, -+ { title: t("Connectors") }, -+ connected.map((connector) => (0, import_react26.createElement)( -+ "div", -+ { key: connector.id, className: "bt-connector-bot" }, -+ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) -+ )) -+ ); ++ ++// src/client/header-effects.js ++function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { ++ if (!doc) return () => { ++ }; ++ const slots = /* @__PURE__ */ new Map(); ++ const run = (id, dep, setup) => { ++ const held = slots.get(id); ++ if (held !== void 0 && held.dep === dep) return; ++ held?.off(); ++ slots.set(id, { dep, off: setup() ?? (() => { ++ }) }); ++ }; ++ let running = false; ++ let again = false; ++ const pass = () => { ++ const list = sessions.getSnapshot(); ++ const state = roster.getSnapshot(); ++ const row = Object.values(list?.byId ?? {}).find((entry) => (entry?.retainedBy?.mainView ?? 0) > 0); ++ const sessionId = row?.id; ++ const bot = sessionId === void 0 ? void 0 : state?.byId?.[sessionId]; ++ const room2 = sessionId === void 0 ? void 0 : state?.roomsById?.[sessionId]; ++ const name = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; ++ run("friend-chat", `${sessionId}|${bot?.remote === true}`, () => { ++ if (!bot?.remote) return void 0; ++ doc.body.dataset.btFriendChat = ""; ++ return () => { ++ delete doc.body.dataset.btFriendChat; ++ }; ++ }); ++ run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { ++ if (!bot?.remote || row?.blank !== true) return void 0; ++ doc.body.dataset.btRemoteChat = "empty"; ++ doc.body.style.setProperty("--bt-remote-empty", JSON.stringify(t("Send a message. Your friend approves unless Auto-send is on."))); ++ return () => { ++ delete doc.body.dataset.btRemoteChat; ++ doc.body.style.removeProperty("--bt-remote-empty"); ++ }; ++ }); ++ run("composer-label", `${sessionId}|${name}`, () => holdComposerLabel(doc, name ? t("Message {name}", { name }) : t("Message"))); ++ run("visit", sessionId, () => { ++ if (sessionId === void 0) return void 0; ++ actions.beginVisit(sessionId); ++ return () => actions.endVisit(sessionId); ++ }); ++ run("face-at-menu", `${sessionId}|${room2?.origin === "face-to-face"}`, () => { ++ if (room2?.origin !== "face-to-face") return void 0; ++ const stop = actions.filterFaceAtMenu?.(sessionId); ++ return typeof stop === "function" ? stop : void 0; ++ }); ++ run("room-flag", `${sessionId}|${room2 !== void 0}`, () => { ++ if (room2 === void 0) return void 0; ++ doc.body.dataset.btRoom = ""; ++ return () => { ++ delete doc.body.dataset.btRoom; ++ }; ++ }); ++ }; ++ const apply2 = () => { ++ if (running) { ++ again = true; ++ return; ++ } ++ running = true; ++ try { ++ pass(); ++ } finally { ++ running = false; ++ } ++ if (again) { ++ again = false; ++ apply2(); ++ } ++ }; ++ apply2(); ++ const offSessions = sessions.subscribe(apply2); ++ const offRoster = roster.subscribe(apply2); ++ return () => { ++ offSessions?.(); ++ offRoster?.(); ++ for (const held of slots.values()) held.off(); ++ slots.clear(); ++ }; +} + - // src/client/memory-panel.js --var import_react17 = require("react"); --var POLL_MS2 = 1500; -+var import_react27 = require("react"); -+var POLL_MS5 = 1500; - var MENU_EXIT_MS2 = 120; - var counts = /* @__PURE__ */ new Map(); - var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,13 +7384,13 @@ function noteCount(botId, count) { - for (const listener of countListeners) listener(); + // src/client/composer.js +-var import_react10 = require("react"); +-var import_react_dom5 = require("react-dom"); ++var import_react21 = require("react"); ++var import_react_dom7 = require("react-dom"); + var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; + var keepComposerFocus = (event) => event.preventDefault(); + function useDictation(onText) { +- const [listening, setListening] = (0, import_react10.useState)(false); +- const [note, setNote] = (0, import_react10.useState)(""); +- const recognition = (0, import_react10.useRef)(null); +- const latest = (0, import_react10.useRef)(onText); ++ const [listening, setListening] = (0, import_react21.useState)(false); ++ const [note, setNote] = (0, import_react21.useState)(""); ++ const recognition = (0, import_react21.useRef)(null); ++ const latest = (0, import_react21.useRef)(onText); + latest.current = onText; +- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); +- (0, import_react10.useEffect)(() => { ++ (0, import_react21.useEffect)(() => () => recognition.current?.abort(), []); ++ (0, import_react21.useEffect)(() => { + if (!note) return void 0; + const timer = setTimeout(() => setNote(""), 2600); + return () => clearTimeout(timer); +@@ -5745,10 +5745,10 @@ function useDictation(onText) { + let interim = ""; + for (let index = event.resultIndex; index < event.results.length; index += 1) { + const result = event.results[index]; +- const text = result[0]?.transcript ?? ""; ++ const text2 = result[0]?.transcript ?? ""; + 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); + }; +@@ -5774,19 +5774,38 @@ function useDictation(onText) { + const stop = () => recognition.current?.stop(); + return { listening, note, start, stop }; } - function useMemoryCount(botId, actions) { -- const [count, setCount] = (0, import_react17.useState)(counts.get(botId) ?? null); -- (0, import_react17.useEffect)(() => { -+ const [count, setCount] = (0, import_react27.useState)(counts.get(botId) ?? null); -+ (0, import_react27.useEffect)(() => { - const listener = () => setCount(counts.get(botId) ?? null); - countListeners.add(listener); - listener(); - let stop = false; -- actions.memoryView?.(botId).then((view) => { -+ void actions.memoryView?.(botId).then((view) => { - if (!stop) noteCount(botId, view.entries.length); - }).catch(() => { - }); -@@ -7404,16 +7404,16 @@ function useMemoryCount(botId, actions) { - function MemoryRow({ bot, actions }) { - const count = useMemoryCount(bot.id, actions); - const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", ++function ComposerIdentity({ sessionId, useRoster }) { ++ const roster = useRoster((value) => value); ++ const room2 = roster.roomsById?.[sessionId]; ++ const self = roster.self; ++ const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); ++ if (room2?.viewerRole === "invited-owner") { ++ return (0, import_react21.createElement)( ++ "span", ++ { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, ++ t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) ++ ); ++ } ++ if (!self || !room2 || humans.length < 2) return null; ++ return (0, import_react21.createElement)( ++ "span", ++ { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, ++ t("Sending as {name}", { name: self.displayName || self.memberId }) ++ ); ++} + function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +- const dictation = useDictation((text, final) => { +- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); ++ const dictation = useDictation((text2, final) => { ++ if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); + }); + const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); +- (0, import_react10.useEffect)(() => { ++ (0, import_react21.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} ++ const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} + + `; +- inputActions.insertText(line, inputActions.captureInsertion()); ++ inputActions.insertText(line2, inputActions.captureInsertion()); + const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); + if (box instanceof HTMLElement) { + box.focus(); +@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + }, [quote?.token]); + if (sessionId === void 0) return null; + const pluginMic = actions.speech() !== void 0; +- return (0, import_react10.createElement)( +- import_react10.Fragment, ++ return (0, import_react21.createElement)( ++ import_react21.Fragment, null, -- (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Memory")), -+ (0, import_react27.createElement)( - "button", - { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -- (0, import_react17.createElement)(MemoryIcon), -- (0, import_react17.createElement)("span", { className: "bt-mem-row-copy" }, label), -- (0, import_react17.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react17.createElement)(ChevronRightIcon)) -+ (0, import_react27.createElement)(MemoryIcon), -+ (0, import_react27.createElement)("span", { className: "bt-mem-row-copy" }, label), -+ (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon)) - ) +- pluginMic ? null : (0, import_react10.createElement)("button", { ++ pluginMic ? null : (0, import_react21.createElement)("button", { + type: "button", + className: "bt-mic", + "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), +@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + title: dictation.listening ? t("Stop dictation") : t("Dictate"), + onMouseDown: keepComposerFocus, + onClick: () => dictation.listening ? dictation.stop() : dictation.start() +- }, (0, import_react10.createElement)(MicIcon)), +- (0, import_react10.createElement)("button", { ++ }, (0, import_react21.createElement)(MicIcon)), ++ (0, import_react21.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 ++ }, (0, import_react21.createElement)(WaveIcon, { size: 18 })), ++ dictation.note ? (0, import_react21.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ); } -@@ -7428,45 +7428,45 @@ function ago(at, now) { - return format.format(-Math.round(hours / 24), "day"); - } - function Skeleton({ label }) { -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-mem-skel", "aria-busy": true }, -- [0, 1].map((block) => (0, import_react17.createElement)( -+ [0, 1].map((block) => (0, import_react27.createElement)( - "div", - { key: block, className: "bt-mem-skel-block" }, -- (0, import_react17.createElement)("span", { className: "bt-mem-skel-head" }), -- (0, import_react17.createElement)("span", null), -- (0, import_react17.createElement)("span", null), -- (0, import_react17.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) -+ (0, import_react27.createElement)("span", { className: "bt-mem-skel-head" }), -+ (0, import_react27.createElement)("span", null), -+ (0, import_react27.createElement)("span", null), -+ (0, import_react27.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) - )), -- label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null -+ label ? (0, import_react27.createElement)("div", { className: "bt-mem-skel-label" }, label) : null - ); + function menuIcons(roster) { +@@ -5825,17 +5825,17 @@ function menuIcons(roster) { + return (row) => { + if (!row.bot) return PeopleIcon; + const id = row.bot.id; +- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); ++ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react21.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); + return marks.get(id); + }; } - function Summary({ bot, view, regenerate }) { - if (view.entries.length === 0) { -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-mem-empty" }, -- (0, import_react17.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react17.createElement)(MemoryIcon)), -- (0, import_react17.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), -- (0, import_react17.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) -+ (0, import_react27.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react27.createElement)(MemoryIcon)), -+ (0, import_react27.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), -+ (0, import_react27.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) - ); - } -- const failed = view.error && !view.summarizing ? (0, import_react17.createElement)( -+ const failed = view.error && !view.summarizing ? (0, import_react27.createElement)( - "div", - { className: "bt-mem-failed", role: "alert" }, -- (0, import_react17.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), -- (0, import_react17.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) -+ (0, import_react27.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), -+ (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) - ) : null; -- if (!view.summary) return (0, import_react17.createElement)(import_react17.Fragment, null, failed, failed ? null : (0, import_react17.createElement)(Skeleton, { label: t("Writing the summary…") })); -- return (0, import_react17.createElement)( -- import_react17.Fragment, -+ if (!view.summary) return (0, import_react27.createElement)(import_react27.Fragment, null, failed, failed ? null : (0, import_react27.createElement)(Skeleton, { label: t("Writing the summary…") })); -+ return (0, import_react27.createElement)( -+ import_react27.Fragment, + function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + const roster = useRoster((value) => value); +- const [open, setOpen] = (0, import_react10.useState)(null); ++ const [open, setOpen] = (0, import_react21.useState)(null); + const [shown, leaving] = useLinger(open, 120); +- const button = (0, import_react10.useRef)(null); +- const menu = (0, import_react10.useRef)(null); +- (0, import_react10.useEffect)(() => { ++ const button = (0, import_react21.useRef)(null); ++ const menu = (0, import_react21.useRef)(null); ++ (0, import_react21.useEffect)(() => { + if (!open) return void 0; + const onDown = (event) => { + if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); +@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); + }; + const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); +- return (0, import_react10.createElement)( +- import_react10.Fragment, ++ return (0, import_react21.createElement)( ++ import_react21.Fragment, null, - failed, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( +- (0, import_react10.createElement)("button", { ++ (0, import_react21.createElement)("button", { + ref: button, + type: "button", + className: "bt-plus", +@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + "aria-expanded": Boolean(open), + onMouseDown: keepComposerFocus, + onClick: toggle +- }, (0, import_react10.createElement)(PlusIcon)), +- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( ++ }, (0, import_react21.createElement)(PlusIcon)), ++ shown ? (0, import_react_dom7.createPortal)((0, import_react21.createElement)( "div", - { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, -- summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react17.createElement)("h3", { key: index }, block.text) : (0, import_react17.createElement)("p", { key: index }, block.text)) -+ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react27.createElement)("h3", { key: index }, block.text) : (0, import_react27.createElement)("p", { key: index }, block.text)) - ) + { + ref: menu, +@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { + style: { left: shown.left, bottom: shown.bottom }, + "data-leaving": leaving || void 0 + }, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), +- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) ++ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react21.createElement)(PaperclipIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), ++ canReference ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react21.createElement)(ChatsIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, ++ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react21.createElement)(ClockIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) + ), document.body) : null ); } -@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { - return groups; - }, []); - function EntryRow({ entry, remove }) { -- const [confirming, setConfirming] = (0, import_react17.useState)(false); -- const [busy, setBusy] = (0, import_react17.useState)(false); -- (0, import_react17.useEffect)(() => { -+ const [confirming, setConfirming] = (0, import_react27.useState)(false); -+ const [busy, setBusy] = (0, import_react27.useState)(false); -+ (0, import_react27.useEffect)(() => { - if (!confirming) return void 0; - const timer = setTimeout(() => setConfirming(false), 3e3); - return () => clearTimeout(timer); -@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) { - setBusy(true); - if (!await remove(entry)) setBusy(false); - }; -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "li", - { className: "bt-mem-entry", "data-busy": busy || void 0 }, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-entry-copy" }, -- (0, import_react17.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), -- meta ? (0, import_react17.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null -+ (0, import_react27.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), -+ meta ? (0, import_react27.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null - ), -- (0, import_react17.createElement)("button", { -+ (0, import_react27.createElement)("button", { - type: "button", - className: "bt-mem-remove", - "data-confirm": confirming || void 0, -@@ -7512,7 +7512,7 @@ function EntryRow({ entry, remove }) { - "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), - title: confirming ? void 0 : t("Remove from memory"), - onClick: click -- }, confirming ? t("Remove") : (0, import_react17.createElement)(TrashIcon)) -+ }, confirming ? t("Remove") : (0, import_react27.createElement)(TrashIcon)) - ); +@@ -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 ? [ ++ ...here.room.members.map((id) => roster.byId[id]).filter(Boolean).map((bot) => face ? { ...bot, label: faceLabel(myOwnerName(roster, live, bot), bot.name) } : bot), ++ ...live.filter((p) => p.kind === "human").map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role })), ++ // A remote seat already listed under its handle (a duplicate participant record) is offered once. ++ ...uniqueBy(live.filter((p) => p.remote), (p) => p.handle || p.id).map((p) => ({ ++ id: p.handle || p.id, ++ // A face room inserts the disambiguated title. An ordinary group inserts the name the ++ // room routes on (#1141): never "@@handle.home (owner)" and never "(Friend)". ++ name: face ? remoteBotTitle(p) : remoteCallName(p), ++ kind: "bot", ++ remote: p, ++ handle: p.handle, ++ label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) ++ })) ++ ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); + const needle = query.trim().toLowerCase(); +- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); ++ const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); + const everyone = t("Everyone"); + if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { +- rows.push({ room: here.room, text: `@${everyone} ` }); ++ 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)))]; ++ const remoteHandles = new Set(remoteMentionRows(roster, "").map((row) => normHandle(row.remote.handle))); ++ return [ ++ ...rows.filter((row) => !(row.bot && remoteHandles.has(normHandle(remoteRecord(row.bot)?.handle)))), ++ ...remoteMentionRows(roster, query) ++ ]; } - function Entries({ bot, view, remove }) { -@@ -7521,82 +7521,82 @@ function Entries({ bot, view, remove }) { - { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, - { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } - ]; -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-mem-entries" }, -- sections.map((section) => (0, import_react17.createElement)( -+ sections.map((section) => (0, import_react27.createElement)( - "section", - { key: section.scope, className: "bt-mem-section" }, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-section-head" }, -- (0, import_react17.createElement)("h3", null, section.title), -- section.hint ? (0, import_react17.createElement)("span", null, section.hint) : null, -- (0, import_react17.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) -+ (0, import_react27.createElement)("h3", null, section.title), -+ section.hint ? (0, import_react27.createElement)("span", null, section.hint) : null, -+ (0, import_react27.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) - ), -- groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react17.createElement)( -- import_react17.Fragment, -+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react27.createElement)( -+ import_react27.Fragment, - { key: group.key }, -- group.topic ? (0, import_react17.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, -- (0, import_react17.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react17.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) -- )) : (0, import_react17.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) -+ group.topic ? (0, import_react27.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, -+ (0, import_react27.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react27.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) -+ )) : (0, import_react27.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) - )) - ); ++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 }); ++var normHandle = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); ++var remoteRecord = (bot) => bot?.remote ? bot.remote === true ? bot : bot.remote : null; ++var myOwnerName = (roster, live, bot) => { ++ const seat = live.find((p) => !p.remote && p.kind !== "human" && (p.id === bot.id || bot.handle && p.handle === bot.handle)); ++ return seatOwnerName(seat?.ownerName, seat?.handle) || cleanPeerName(roster.self?.displayName, ""); ++}; ++var uniqueBy = (list, key) => { ++ const seen = /* @__PURE__ */ new Set(); ++ return list.filter((item) => { ++ const k = key(item); ++ if (!k || seen.has(k)) return !k; ++ seen.add(k); ++ return true; ++ }); ++}; ++var faceLabel = (person, bot) => person ? `${bot} (${person})` : bot; + function chatRows(roster, list, sessionId) { + const here = conversationOf(roster, sessionId); + if (here === null) return []; +@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { + order: -20, + showGroupTitle: false, + async candidates(session, { query }) { +- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ +- name: row.bot ? row.bot.name : t("Everyone"), +- ...row.bot?.role ? { description: row.bot.role } : {}, +- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, ++ const snapshot = roster.getSnapshot(); ++ const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); ++ const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ ++ // A friend's Bot by the one name the row inserts, "Chief (Bob)" (#967); its handle is the ++ // second line (#384). A seat carries its own name; a friend's chat is named as its sidebar ++ // row and header (#498). ++ name: row.bot?.remote && !face ? row.bot.kind === "bot" ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? row.text.replace(/^@/, "").trimEnd() : t("Everyone"), ++ ...row.bot?.role && !face ? { description: row.bot.role } : {}, ++ ...row.remote ? { description: row.remote.handle } : {}, ++ ...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 + })); ++ if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; ++ const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ ++ name: row.name, ++ ...row.description ? { description: row.description } : {}, ++ icon: row.icon ?? "session", ++ section: row.section ?? "FlyBot", ++ value: row.value ++ })); ++ return [...team, ...extra]; + }, + onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 + }; +@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { + return [bots, chats]; } - function changeLine(change) { -- const text = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; -- if (!change.ok) return t("Not changed: {text} ({reason})", { text, reason: hostText(change.error ?? "") }); -- if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text }) : t("Removed: {text}", { text }); -- if (change.op === "edit") return t("Updated: {text}", { text }); -- return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); -+ const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; -+ if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); -+ if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); -+ if (change.op === "edit") return t("Updated: {text}", { text: text2 }); -+ return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 }); + ++// src/client/face-at-filter.js ++var REFERENCE_SOURCE = "reference"; ++var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; ++var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; ++var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; ++function planFaceMenu(state, previous = null) { ++ const hidden = []; ++ const ready = []; ++ let shown = 0; ++ let loading = false; ++ for (const group of state?.groups ?? []) { ++ const items = group.items ?? []; ++ if (group.status !== "ready" && items.length === 0) loading = true; ++ items.forEach((item, index) => { ++ const hide = isSessionRow(group, item); ++ if (hide) hidden.push({ source: group.source, index, title: item.section !== void 0 && item.section !== items[index - 1]?.section }); ++ else shown += 1; ++ if (group.status === "ready") ready.push({ source: group.source, index, hide }); ++ }); ++ } ++ const open = state?.open === true; ++ const blank3 = open && hidden.length > 0 && shown === 0 && !loading; ++ const dismiss = blank3 && (state.groups ?? []).every((group) => group.status === "ready"); ++ return { hidden, blank: blank3, dismiss, target: dismiss ? null : steer(ready, state?.highlight ?? null, previous) }; ++} ++function steer(ready, highlight, previous) { ++ const at = ready.findIndex((row) => same(row, highlight)); ++ if (at < 0 || !ready[at].hide) return null; ++ const from = ready.findIndex((row) => same(row, previous)); ++ const n = ready.length; ++ const dir = from >= 0 && from !== at && (from - at + n) % n === 1 ? -1 : 1; ++ for (let step = 1; step < n; step += 1) { ++ const row = ready[((at + dir * step) % n + n) % n]; ++ if (!row.hide) return { source: row.source, index: row.index }; ++ } ++ return null; ++} ++function faceMenuCss({ hidden, blank: blank3 }) { ++ if (hidden.length === 0) return ""; ++ const rows = hidden.map((row) => `[data-trigger-menu] [id="${optionId(row.source, row.index)}"]`); ++ const titles = hidden.filter((row) => row.title).map((row) => `[data-trigger-menu] [role="presentation"]:has(+ [id="${optionId(row.source, row.index)}"])`); ++ const css = [`${[...rows, ...titles].join(",")}{display:none!important}`]; ++ if (blank3) css.push("[data-trigger-menu]{display:none!important}"); ++ return css.join("\n"); ++} ++function watchFaceMenu(controller, doc = globalThis.document) { ++ if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { ++ }; ++ const style = doc.createElement("style"); ++ style.setAttribute("data-bt-face-at", ""); ++ doc.head.appendChild(style); ++ let previous = null; ++ let stopped = false; ++ const later = typeof queueMicrotask === "function" ? queueMicrotask : (fn) => Promise.resolve().then(fn); ++ const apply2 = () => { ++ if (stopped) return; ++ const state = controller.menu.getSnapshot(); ++ const plan = planFaceMenu(state, previous); ++ const css = faceMenuCss(plan); ++ if (style.textContent !== css) style.textContent = css; ++ if (plan.dismiss) later(() => { ++ if (!stopped && planFaceMenu(controller.menu.getSnapshot(), previous).dismiss) controller.dismiss(); ++ }); ++ else if (plan.target) later(() => { ++ if (!stopped) controller.hover(plan.target.source, plan.target.index); ++ }); ++ else previous = state.open ? state.highlight : null; ++ }; ++ const unsubscribe = controller.menu.subscribe(apply2); ++ apply2(); ++ return () => { ++ stopped = true; ++ unsubscribe?.(); ++ style.remove(); ++ }; ++} ++ + // 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); + }; + ascii(0, "RIFF"); + view.setUint32(4, 36 + samples.length * 2, true); +@@ -6030,9 +6030,9 @@ function waveBytes(samples, rate = 16e3) { + return bytes; } - function Reply({ answer, dismiss }) { -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-mem-reply", role: "status" }, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-reply-head" }, -- (0, import_react17.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), -- answer.pending ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react17.createElement)(CloseIcon)) -+ (0, import_react27.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), -+ answer.pending ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react27.createElement)(CloseIcon)) - ), -- answer.pending ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react17.createElement)( -- import_react17.Fragment, -+ answer.pending ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react27.createElement)( -+ import_react27.Fragment, - null, -- answer.reply ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, -- answer.changes.length > 0 ? (0, import_react17.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react17.createElement)( -+ answer.reply ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, -+ answer.changes.length > 0 ? (0, import_react27.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react27.createElement)( - "li", - { key: index, "data-ok": change.ok || void 0 }, -- change.ok ? (0, import_react17.createElement)(CheckIcon) : (0, import_react17.createElement)(CloseIcon), -- (0, import_react17.createElement)("span", null, changeLine(change)) -+ change.ok ? (0, import_react27.createElement)(CheckIcon) : (0, import_react27.createElement)(CloseIcon), -+ (0, import_react27.createElement)("span", null, changeLine(change)) - ))) : null - ) - ); + 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); } - function useMemoryView(botId, actions) { -- const [view, setView] = (0, import_react17.useState)(null); -- const [error, setError] = (0, import_react17.useState)(""); -- const timer = (0, import_react17.useRef)(0); -- const latest = (0, import_react17.useRef)(0); -- const alive = (0, import_react17.useRef)(true); -- const show = (0, import_react17.useCallback)((next) => { -+ const [view, setView] = (0, import_react27.useState)(null); -+ const [error, setError] = (0, import_react27.useState)(""); -+ const timer = (0, import_react27.useRef)(0); -+ const latest = (0, import_react27.useRef)(0); -+ const alive = (0, import_react27.useRef)(true); -+ const show = (0, import_react27.useCallback)((next) => { - setView(next); - setError(""); - noteCount(botId, next.entries.length); - }, [botId]); -- const load = (0, import_react17.useCallback)(async (flags = {}) => { -+ const load = (0, import_react27.useCallback)(async (flags = {}) => { - clearTimeout(timer.current); - const ticket = ++latest.current; - try { - const next = await actions.memoryView(botId, flags); - if (!alive.current || ticket !== latest.current) return; - show(next); -- if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS2); -+ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5); - } catch (failure) { - if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure)); - } - }, [botId]); -- (0, import_react17.useEffect)(() => { -+ (0, import_react27.useEffect)(() => { - alive.current = true; - void load({ summarize: true }); - return () => { -@@ -7606,9 +7606,9 @@ function useMemoryView(botId, actions) { - }, [botId]); - return { view, error, load, show }; - } --function useNow(every) { -- const [now, setNow] = (0, import_react17.useState)(Date.now()); -- (0, import_react17.useEffect)(() => { -+function useNow3(every) { -+ const [now, setNow] = (0, import_react27.useState)(Date.now()); -+ (0, import_react27.useEffect)(() => { - const id = setInterval(() => setNow(Date.now()), every); - return () => clearInterval(id); - }, [every]); -@@ -7616,18 +7616,18 @@ function useNow(every) { - } - function MemoryDialog({ bot, actions, leaving = false }) { - const { view, error, load, show } = useMemoryView(bot.id, actions); -- const [page, setPage] = (0, import_react17.useState)("summary"); -- const [menu, setMenu] = (0, import_react17.useState)(false); -+ const [page, setPage] = (0, import_react27.useState)("summary"); -+ const [menu, setMenu] = (0, import_react27.useState)(false); - const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); -- const [text, setText] = (0, import_react17.useState)(""); -- const [answer, setAnswer] = (0, import_react17.useState)(null); -- const menuRef = (0, import_react17.useRef)(null); -- const dotsRef = (0, import_react17.useRef)(null); -- const inputRef = (0, import_react17.useRef)(null); -- const now = useNow(3e4); -- const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); -+ const [text2, setText] = (0, import_react27.useState)(""); -+ const [answer, setAnswer] = (0, import_react27.useState)(null); -+ const menuRef = (0, import_react27.useRef)(null); -+ const dotsRef = (0, import_react27.useRef)(null); -+ const inputRef = (0, import_react27.useRef)(null); -+ const now = useNow3(3e4); -+ const close = (0, import_react27.useCallback)(() => actions.closeMemory(), []); - useEscape(close); -- (0, import_react17.useEffect)(() => { -+ (0, import_react27.useEffect)(() => { - if (!menu) return void 0; - const onDown = (event) => { - if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7671,7 +7671,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { - }; - const ask = async (event) => { - event.preventDefault(); -- const asked = text.trim(); -+ const asked = text2.trim(); - if (asked === "" || answer?.pending) return; - setText(""); - setAnswer({ asked, pending: true }); -@@ -7689,65 +7689,65 @@ function MemoryDialog({ bot, actions, leaving = false }) { - const when3 = view?.summary ? ago(view.summary.at, now) : null; - const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; - const busy = answer?.pending === true; -- return (0, import_react17.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, -- (0, import_react17.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-head" }, -- page === "entries" ? (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react17.createElement)(ChevronLeftIcon)) : null, -- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }), -- (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)), -- (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)), -- menuShown ? (0, import_react17.createElement)( -+ page === "entries" ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react27.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react27.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -+ (0, import_react27.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -+ (0, import_react27.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react27.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react27.createElement)(DotsIcon)), -+ (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react27.createElement)(CloseIcon)), -+ menuShown ? (0, import_react27.createElement)( - "div", - { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "button", - { type: "button", role: "menuitem", onClick: openEntries }, -- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -- (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, String(count)) -+ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -+ (0, import_react27.createElement)("span", { className: "bt-menu-hint" }, String(count)) - ), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "button", - { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, -- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) -+ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) - ) - ) : null - ), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-body" }, -- error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -- view === null ? error ? null : (0, import_react17.createElement)(Skeleton) : page === "entries" ? (0, import_react17.createElement)(Entries, { bot, view, remove }) : (0, import_react17.createElement)(Summary, { bot, view, regenerate }) -+ error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -+ view === null ? error ? null : (0, import_react27.createElement)(Skeleton) : page === "entries" ? (0, import_react27.createElement)(Entries, { bot, view, remove }) : (0, import_react27.createElement)(Summary, { bot, view, regenerate }) - ), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-mem-foot" }, -- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -- (0, import_react17.createElement)( -+ answer ? (0, import_react27.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -+ (0, import_react27.createElement)( - "form", - { className: "bt-mem-ask", onSubmit: ask }, -- (0, import_react17.createElement)("input", { -+ (0, import_react27.createElement)("input", { - ref: inputRef, - className: "bt-mem-input", -- value: text, -+ value: text2, - maxLength: 2e3, - disabled: view === null, - placeholder: t("Ask or update"), - "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), - onChange: (event) => setText(event.target.value) - }), -- (0, import_react17.createElement)( -+ (0, import_react27.createElement)( - "button", -- { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, -- busy ? (0, import_react17.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react17.createElement)(ArrowUpIcon) -+ { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") }, -+ busy ? (0, import_react27.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react27.createElement)(ArrowUpIcon) - ) - ) - ) -@@ -7755,8 +7755,40 @@ function MemoryDialog({ bot, actions, leaving = false }) { - ); + var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); + async function openCapture() { +@@ -6121,233 +6121,9 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { + }; } -+// src/client/standing-approvals.js -+var import_react28 = require("react"); -+var standingOf = (target, roster) => { -+ const me = roster?.self?.memberId; -+ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); -+}; -+function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card2 }) { -+ const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; -+ if (grants.length === 0) return null; -+ const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); -+ return (0, import_react28.createElement)( -+ Card2, -+ { title: t("Always allowed") }, -+ grants.map((grant) => (0, import_react28.createElement)( -+ "div", -+ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, -+ (0, import_react28.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react28.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), -+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) -+ ), -+ (0, import_react28.createElement)("button", { -+ type: "button", -+ className: "bt-soft bt-soft-sm", -+ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), -+ onClick: () => run(actions.removeStandingApproval(scope, target.id, grant.kind)) -+ }, t("Remove")) -+ )) -+ ); -+} -+ - // src/client/bot-settings.js --var import_react18 = require("react"); -+var import_react29 = require("react"); - var capitalize = (word) => word[0].toUpperCase() + word.slice(1); - async function avatarImage(file) { - if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +7780,8 @@ async function avatarImage(file) { - throw new Error(t("That image is too large")); - } - function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { -- const [draft, setDraft] = (0, import_react18.useState)(null); -- const reverting = (0, import_react18.useRef)(false); -+ const [draft, setDraft] = (0, import_react29.useState)(null); -+ const reverting = (0, import_react29.useRef)(false); - const commit = () => { - const next = draft?.trim(); - setDraft(null); -@@ -7792,7 +7792,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu - if (next === void 0 || next === (value ?? "") || required && next === "") return; - void onSave(next); - }; -- return (0, import_react18.createElement)(multiline ? "textarea" : "input", { -+ return (0, import_react29.createElement)(multiline ? "textarea" : "input", { - className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", - value: draft ?? value ?? "", - placeholder, -@@ -7813,29 +7813,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu - }); - } - function Row({ label, hint, stack = false, children }) { -- return (0, import_react18.createElement)( -+ return (0, import_react29.createElement)( - "label", - { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), - children - ); - } - function Card({ title, children }) { -- return (0, import_react18.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react18.createElement)("h3", null, title) : null, (0, import_react18.createElement)("div", { className: "bt-set-card" }, children)); -+ return (0, import_react29.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react29.createElement)("h3", null, title) : null, (0, import_react29.createElement)("div", { className: "bt-set-card" }, children)); - } - function Switch({ on, label, disabled, onChange }) { -- return (0, import_react18.createElement)( -+ return (0, import_react29.createElement)( - "button", - { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, -- (0, import_react18.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) -+ (0, import_react29.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) - ); - } --function SwitchRow({ label, hint, on, disabled, onChange }) { -- return (0, import_react18.createElement)( -+function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { -+ return (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- (0, import_react18.createElement)(Switch, { on, label, disabled, onChange }) -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ (0, import_react29.createElement)(Switch, { on, label, disabled, onChange }) - ); - } - function modelHint(bot, roster) { -@@ -7846,22 +7846,22 @@ function modelHint(bot, roster) { - return t("Applies from the next message."); - } - function ShapeRing({ shape, size }) { -- const maskId = (0, import_react18.useRef)(""); -+ const maskId = (0, import_react29.useRef)(""); - if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; - const def = shapeOf(shape); - const unit = def.size / size; - const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; -- return (0, import_react18.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); -+ return (0, import_react29.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); - } - function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { - const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; - if (!shapes.includes(look.shape)) shapes.push(look.shape); - const rows = []; - for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); -- return (0, import_react18.createElement)( -- import_react18.Fragment, -+ return (0, import_react29.createElement)( -+ import_react29.Fragment, - null, -- (0, import_react18.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react18.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react18.createElement)("button", { -+ (0, import_react29.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react29.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react29.createElement)("button", { - key: id, - type: "button", - className: "bt-shape", -@@ -7869,8 +7869,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { - title: t(shapeOf(id).label), - "aria-pressed": !hasPhoto && look.shape === id, - onClick: () => onShape(id) -- }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -- (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", { -+ }, (0, import_react29.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react29.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -+ (0, import_react29.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react29.createElement)("button", { - key: color, - type: "button", - className: "bt-color", -@@ -7878,19 +7878,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { - title: t(capitalize(color)), - "aria-pressed": look.color === color, - onClick: () => onColor(color) -- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } })))) -+ }, (0, import_react29.createElement)("span", { style: { background: paintCss(color) } })))) - ); - } - function AvatarEditor({ bot, actions, run, onClose }) { - const look = lookOf(bot); -- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot"); -- const [over, setOver] = (0, import_react18.useState)(false); -- const ref = (0, import_react18.useRef)(null); -- const fileRef = (0, import_react18.useRef)(null); -+ const [tab, setTab] = (0, import_react29.useState)(look.image ? "upload" : "bot"); -+ const [over, setOver] = (0, import_react29.useState)(false); -+ const ref = (0, import_react29.useRef)(null); -+ const fileRef = (0, import_react29.useRef)(null); - const upload = (file) => { - if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); - }; -- (0, import_react18.useEffect)(() => { -+ (0, import_react29.useEffect)(() => { - const onDown = (event) => { - if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; - onClose(); -@@ -7908,7 +7908,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { - window.removeEventListener("keydown", onKey, true); - }; - }, [onClose]); -- (0, import_react18.useEffect)(() => { -+ (0, import_react29.useEffect)(() => { - if (tab !== "upload") return void 0; - const onPaste = (event) => { - const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +7922,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { - }, [tab, bot]); - const hasPhoto = Boolean(look.image); - const glide = useGlide(tab); -- return (0, import_react18.createElement)( -+ return (0, import_react29.createElement)( - "div", - { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-avedit-top" }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, -- (0, import_react18.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react18.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) -+ (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react29.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) - ), -- (0, import_react18.createElement)("button", { -+ (0, import_react29.createElement)("button", { - type: "button", - className: "bt-avedit-tab bt-avedit-reset", - title: t("Reset character to default"), - onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) - }, t("Reset")) - ), -- tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, { -+ tab === "bot" ? (0, import_react29.createElement)(CharacterGrid, { - look, - hasPhoto, - onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), - onColor: (color) => run(actions.updateBot(bot.id, { color })) - }) : null, -- tab === "upload" ? (0, import_react18.createElement)( -+ tab === "upload" ? (0, import_react29.createElement)( - "div", - { - className: "bt-dropzone", -@@ -7963,15 +7963,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { - upload(event.dataTransfer.files?.[0]); - } - }, -- hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null, -- (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")), -- (0, import_react18.createElement)( -+ hasPhoto ? (0, import_react29.createElement)("img", { src: look.image, alt: "" }) : null, -+ (0, import_react29.createElement)("div", null, t("Drag, drop, or paste an image")), -+ (0, import_react29.createElement)( - "div", - { className: "bt-actions", style: { justifyContent: "center" } }, -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -- hasPhoto ? (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null -+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -+ hasPhoto ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null - ), -- (0, import_react18.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ (0, import_react29.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { - const file = event.target.files?.[0]; - event.target.value = ""; - upload(file); -@@ -7982,26 +7982,27 @@ function AvatarEditor({ bot, actions, run, onClose }) { - function MemoryCard({ bot, actions }) { - const count = useMemoryCount(bot.id, actions); - const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -- return (0, import_react18.createElement)( -+ return (0, import_react29.createElement)( - Card, - { title: t("Memory") }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label)), -- (0, import_react18.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react18.createElement)(ChevronRightIcon)) -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label)), -+ (0, import_react29.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react29.createElement)(ChevronRightIcon)) - ) - ); - } - function BotSettingsForm({ bot, roster, actions, run }) { -- const [editing, setEditing] = (0, import_react18.useState)(false); -- const [confirming, setConfirming] = (0, import_react18.useState)(false); -- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []); -- (0, import_react18.useEffect)(() => { -+ const [editing, setEditing] = (0, import_react29.useState)(false); -+ const [confirming, setConfirming] = (0, import_react29.useState)(false); -+ const closeEditor = (0, import_react29.useCallback)(() => setEditing(false), []); -+ (0, import_react29.useEffect)(() => { - if (!confirming) return void 0; - const timer = setTimeout(() => setConfirming(false), 4e3); - return () => clearTimeout(timer); - }, [confirming]); -+ if (bot.remote) return null; - const isMainBot = isMainOf(roster, bot.id); - const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; - const isTile = roster.mainBotId === bot.id; -@@ -8011,150 +8011,813 @@ function BotSettingsForm({ bot, roster, actions, run }) { - if (confirming) void run(actions.deleteBot(bot.id)); - else setConfirming(true); - }; -- (0, import_react18.useEffect)(() => { -+ (0, import_react29.useEffect)(() => { - void actions.refreshModels?.(); - }, [bot.id]); -- return (0, import_react18.createElement)( -+ return (0, import_react29.createElement)( - "div", - { className: "bt-bs" }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-bs-hero" }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-avatar-anchor" }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), -- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) -+ (0, import_react29.createElement)(BotMark, { bot, size: 64, live: true }), -+ (0, import_react29.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react29.createElement)(PencilIcon)) - ), -- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null -+ editing ? (0, import_react29.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null - ), -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-bs-who" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) -+ (0, import_react29.createElement)("span", { className: "bt-bs-name" }, bot.name), -+ (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) - ) - ), -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - Card, - { title: t("Profile") }, -- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) -+ (0, import_react29.createElement)(Row, { label: t("Name") }, (0, import_react29.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -+ (0, import_react29.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react29.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) - ), -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - Card, - { title: t("Model") }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) -- ) -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -+ (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run }) -+ ), -+ bot.model ? (0, import_react29.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null - ), -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - Card, - { title: t("Working folder") }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -+ (0, import_react29.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) - ) - ), -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - Card, - { title: t("Instructions") }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( - Row, - { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) -+ (0, import_react29.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) - ) - ), -- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), -- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), -- (0, import_react18.createElement)( -+ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). -+ (0, import_react29.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react29.createElement)(Card, { title: t("Sharing") }, children) }), -+ (0, import_react29.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -+ (0, import_react29.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), -+ roster.memory === false ? null : (0, import_react29.createElement)(MemoryCard, { bot, actions }), -+ (0, import_react29.createElement)(BotSchedules, { bot, roster, actions }), -+ (0, import_react29.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), -+ (0, import_react29.createElement)( - Card, - { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( -+ onlyMain ? (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( -+ (0, import_react29.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -+ ), -+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -+ ) : (0, import_react29.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) -+ ), -+ // The Main Bot tile is always first and always shown, like in the context menu. -+ isTile ? null : (0, import_react29.createElement)( -+ Card, -+ { title: t("Sidebar") }, -+ (0, import_react29.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -+ isMainBot ? null : (0, import_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -+ ), -+ (0, import_react29.createElement)( -+ Card, -+ { title: t("More") }, -+ (0, import_react29.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -+ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) -+ ), -+ isMainBot ? null : (0, import_react29.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -+ (0, import_react29.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -+ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -+ ), -+ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) -+ ) -+ ) -+ ); -+} -+var BOT_SETTINGS_CSS = ` -+.bt-bs{display:flex;flex-direction:column;gap:18px} -+.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} -+.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} -+.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-bs .bt-avedit{left:0;transform:none} -+.bt-bs-row{min-height:48px} -+label.bt-bs-row{cursor:text} -+.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} -+/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to -+ break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test -+ connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under -+ a label is prose, so it wraps and is never cut: the row grows instead. */ -+.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} -+.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} -+.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} -+.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} -+${MODEL_TEST_CSS} -+.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} -+.bt-bs-input:hover{background:var(--bt-hover)} -+.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -+.bt-bs-input::placeholder{color:var(--bt-ink-3)} -+.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} -+.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} -+.bt-bs-area:hover{background:var(--bt-active)} -+.bt-bs-danger .bt-bs-label{color:#c21d2e} -+.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} -+.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} -+.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} -+.bt-switch[aria-checked=true]{background:var(--bt-accent)} -+.bt-switch:disabled{opacity:.45;cursor:default} -+.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} -+.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} -+.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} -+.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} -+.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} -+.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} -+@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} -+/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below -+ 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and -+ keeps its own words. Nothing is clipped to fit a width that is not there. */ -+@container bt-panel (max-width:300px){ -+ .bt-bs-row{flex-wrap:wrap;row-gap:6px} -+ .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} -+ .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} -+} -+`; -+ -+// src/client/group-settings.js -+var import_react30 = require("react"); -+var ROOM_MODES = [ -+ ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], -+ ["everyone", "Everyone", "Every member answers in turn, admin first."] -+]; -+var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; -+function faceSeatBot(seat, roster) { -+ if (!seat) return void 0; -+ const record = seat.record && typeof seat.record === "object" ? seat.record : {}; -+ const extra = {}; -+ if (typeof record.color === "string" && record.color) extra.color = record.color; -+ if (record.avatar && typeof record.avatar === "object") extra.avatar = record.avatar; -+ if (seat.local && !seat.local.remote) return Object.keys(extra).length ? { ...seat.local, ...extra } : seat.local; -+ const handle = typeof record.handle === "string" && record.handle.trim() ? record.handle : typeof seat.key === "string" && seat.key.startsWith("@") ? seat.key : ""; -+ return lookBotForRemote({ -+ handle, -+ displayName: record.displayName || seat.bot, -+ name: seat.bot, -+ label: record.label, -+ id: record.id || seat.key, -+ ...extra -+ }, roster); -+} -+function faceMemberLook(bot) { -+ if (!bot) return void 0; -+ return bot.remote ? lookAvoidingLocals(bot) : lookOf(bot); -+} -+function faceMarks(person, roster, size) { -+ const marks = []; -+ for (const seat of person?.seats ?? []) { -+ const bot = faceSeatBot(seat, roster); -+ if (!bot) continue; -+ marks.push((0, import_react30.createElement)(BotMark, { key: seat.key, bot, size, live: false })); -+ } -+ if (marks.length === 0) return [(0, import_react30.createElement)(PersonMark, { key: "person", size, name: person?.name })]; -+ return marks; -+} -+function faceRoomCount(room2, roster) { -+ const people2 = facePeople(room2, roster).length; -+ const seats = faceSeats(room2, roster).length; -+ const persons = people2 === 1 ? t("1 person") : t("{count} people", { count: people2 }); -+ const bots = seats === 1 ? t("1 Bot") : t("{count} Bots", { count: seats }); -+ return `${persons} · ${bots}`; -+} -+var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); -+var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); -+function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { -+ const bots = person.seats.flatMap((seat, index) => { -+ const name = seat.local && onOpenBot ? (0, import_react30.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react30.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); -+ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react30.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; -+ return index === 0 ? marked : [(0, import_react30.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; -+ }); -+ return (0, import_react30.createElement)( -+ "div", -+ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-gs-who bt-face-person" }, -+ (0, import_react30.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-face-person-text" }, -+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), -+ (0, import_react30.createElement)("span", { className: "bt-face-bots" }, bots) -+ ) -+ ) -+ ); -+} -+function FaceLead({ room: room2, roster, pick: pick2 }) { -+ const lead = room2.face?.lead; -+ const [open, setOpen] = (0, import_react30.useState)(false); -+ const [busy, setBusy] = (0, import_react30.useState)(false); -+ const [error, setError] = (0, import_react30.useState)(""); -+ const seats = faceSeats(room2, roster); -+ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); -+ if (!current) return null; -+ const canPick = lead.canPick === true && typeof pick2 === "function"; -+ const choose = async (handle) => { -+ setBusy(true); -+ setError(""); -+ try { -+ await pick2(handle); -+ setOpen(false); -+ } catch (err) { -+ setError(err?.message || t("Could not change the room lead.")); -+ } finally { -+ setBusy(false); -+ } -+ }; -+ return (0, import_react30.createElement)( -+ "div", -+ { className: "bt-face-lead", "data-flybot": "face-lead" }, -+ (0, import_react30.createElement)("div", { className: "bt-section-title" }, t("Room lead")), -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row" }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) -+ ), -+ canPick ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null -+ ), -+ open && canPick ? seats.map((seat) => (0, import_react30.createElement)( -+ "div", -+ { key: seat.key, className: "bt-set-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), -+ seat === current ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) -+ )) : null, -+ open && canPick && lead.picked ? (0, import_react30.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, -+ canPick || !lead.supported ? null : (0, import_react30.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), -+ error ? (0, import_react30.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null -+ ); -+} -+function seatedCount(room2) { -+ const live = room2?.presentedParticipants ?? room2?.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length; -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length; -+ return humans + remotes + (room2?.members?.length ?? 0); -+} -+function roomSubtitle(room2, roster) { -+ if (room2.origin === "face-to-face") return faceRoomCount(room2, roster); -+ const admin = roster.byId[room2.admin]; -+ const count = seatedCount(room2); -+ return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); -+} -+function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { -+ const [adding, setAdding] = (0, import_react30.useState)(false); -+ const [confirming, setConfirming] = (0, import_react30.useState)(false); -+ (0, import_react30.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(false), 4e3); -+ return () => clearTimeout(timer); -+ }, [confirming]); -+ const live = room2.presentedParticipants ?? room2.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); -+ const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -+ const face = room2.origin === "face-to-face"; -+ const people2 = face ? facePeople(room2, roster) : []; -+ const shared2 = humans.length > 1; -+ const chiefs = new Set((roster.humans ?? []).map((person) => person.designatedChiefId).filter(Boolean)); -+ const outside = roster.bots.filter((bot) => { -+ if (bot.remote) return false; -+ if (room2.members.includes(bot.id)) return false; -+ if (shared2) return chiefs.has(bot.id); -+ return true; -+ }); -+ const outsidePeople = (roster.humans ?? []).filter((person) => !humans.some((p) => p.id === person.id)); -+ const outsideRemotes = (roster.remoteBots ?? []).filter((bot) => !remotes.some((p) => p.handle === bot.handle)); -+ const admin = roster.byId[room2.admin]; -+ const mode = shownMode(room2, roster); -+ const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; -+ const update = (patch) => run(actions.updateRoom(room2.id, patch)); -+ const openBot = (id) => { -+ actions.closeSettings(); -+ actions.openSession(id); -+ }; -+ const remove = () => { -+ if (!confirming) { -+ setConfirming(true); -+ return; -+ } -+ void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { -+ if (done) onDeleted?.(); -+ }); -+ }; -+ return (0, import_react30.createElement)( -+ "div", -+ { className: "bt-bs" }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-bs-hero" }, -+ (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-bs-who" }, -+ (0, import_react30.createElement)("span", { className: "bt-bs-name" }, room2.name), -+ (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -+ ) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Profile") }, -+ (0, import_react30.createElement)( -+ Row, -+ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -+ (0, import_react30.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) -+ ), -+ (0, import_react30.createElement)( -+ Row, -+ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -+ (0, import_react30.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) -+ ) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Who answers") }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react30.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react30.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_react30.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -+ ) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Working folder") }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -+ (0, import_react30.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -+ ) -+ ), -+ face ? (0, import_react30.createElement)( -+ Card, -+ { title: t("Members · {count}", { count: people2.length }) }, -+ people2.map((person) => (0, import_react30.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) -+ ) : null, -+ face ? null : (0, import_react30.createElement)( -+ Card, -+ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, -+ humans.map((person) => (0, import_react30.createElement)( -+ "div", -+ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-gs-who" }, -+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), -+ (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Human")), -+ person.role === "admin" ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -+ ), -+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null -+ )), -+ members.map((member) => { -+ const leads = room2.admin === member.id; -+ const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; -+ const taskLead = room2.taskLead === member.id; -+ return (0, import_react30.createElement)( -+ "div", -+ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -+ (0, import_react30.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, member.name), -+ leads ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, -+ taskLead ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, -+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Chief")) : null -+ ), -+ leads ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -+ boardHere ? taskLead ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, -+ members.length > 2 ? (0, import_react30.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 -+ ); -+ }), -+ remotes.map((person) => (0, import_react30.createElement)( -+ "div", -+ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-gs-who" }, -+ (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react30.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), -+ // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is -+ // labelled as such; a Bot with no Bot Social identity can never be written to at all. -+ room2.origin === "face-to-face" ? null : (0, import_react30.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) - ), -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) -+ // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. -+ room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") -+ )), -+ room2.origin === "face-to-face" ? null : adding ? [ -+ ...outsidePeople.map((person) => (0, import_react30.createElement)( -+ "button", -+ { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -+ setAdding(false); -+ void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); -+ } }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add person")) -+ )), -+ ...outside.map((bot) => (0, import_react30.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_react30.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, bot.name)), -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add")) -+ )), -+ ...outsideRemotes.map((bot) => (0, import_react30.createElement)( -+ "button", -+ { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -+ setAdding(false); -+ void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); -+ } }, -+ (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) -+ )), -+ (0, import_react30.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -+ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react30.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react30.createElement)(PlusIcon), t("Add member")) : null, -+ (0, import_react30.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-hint" }, -+ // The host's two-Bot floor guards membership changes; this hint warns before one. -+ members.length + humans.length + remotes.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.")}${room2.origin === "face-to-face" || room2.origin === "inbound-share" ? "" : ` ${t("The task lead divides the board. When unset, the admin does.")}`}` -+ )), -+ // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after -+ // the note on who leads, so that note stays with the members it is about (#892). -+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react30.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) - ), -- // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react18.createElement)( -+ (0, import_react30.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), -+ (0, import_react30.createElement)( - Card, - { title: t("Sidebar") }, -- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -+ (0, import_react30.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_react30.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_react18.createElement)( -+ (0, import_react30.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) -- ), -- isMainBot ? null : (0, import_react18.createElement)( -+ { title: t("More") }, -+ (0, import_react30.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( -+ (0, import_react30.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -+ (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -+ (0, import_react30.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_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) -+ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) - ) - ) - ); - } --var BOT_SETTINGS_CSS = ` --.bt-bs{display:flex;flex-direction:column;gap:18px} --.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} --.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} --.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-bs .bt-avedit{left:0;transform:none} --.bt-bs-row{min-height:48px} --label.bt-bs-row{cursor:text} --.bt-bs-row>.bt-set-copy{flex:1 1 auto} --.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} --.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} --.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} --.bt-bs-input:hover{background:var(--bt-hover)} --.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} --.bt-bs-input::placeholder{color:var(--bt-ink-3)} --.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} --.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} --.bt-bs-area:hover{background:var(--bt-active)} --.bt-bs-danger .bt-bs-label{color:#c21d2e} --.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} --.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} --.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} --.bt-switch[aria-checked=true]{background:var(--bt-accent)} --.bt-switch:disabled{opacity:.45;cursor:default} --.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} --.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} --.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} --.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} --.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} --.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} --@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} -+function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -+ const [admin, setAdmin] = (0, import_react30.useState)(null); -+ const [members, setMembers] = (0, import_react30.useState)([]); -+ const [name, setName] = (0, import_react30.useState)(""); -+ const [busy, setBusy] = (0, import_react30.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, ...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_react30.createElement)("button", { -+ key: bot.id, -+ type: "button", -+ role, -+ className: "bt-ng-chip", -+ "aria-checked": on, -+ onClick -+ }, (0, import_react30.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react30.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react30.createElement)(AdminStar) : null); -+ return (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-card bt-ng" }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react30.createElement)(AdminStar), t("Choose the group admin"))), -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -+ roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) -+ ) -+ ), -+ admin === null ? null : (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -+ (0, import_react30.createElement)( -+ "div", -+ { 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")) -+ ) -+ ), -+ admin === null || members.length === 0 ? null : (0, import_react30.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-ng-step" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react30.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -+ ), -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-task-foot" }, -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) -+ ) -+ ); -+} -+var GROUP_SETTINGS_CSS = ` -+.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -+.bt-ng-step{animation:bt-veil-in .22s ease-out} -+.bt-ng-label{display:inline-flex;align-items:center;gap:6px} -+.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} -+.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} -+.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-ng-chip:active{scale:.96} -+.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} -+.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} -+.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} -+.bt-gs-member{gap:8px} -+.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} -+.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} -+.bt-gs-add{gap:8px;color:var(--bt-ink-2)} -+.bt-face-marks{display:inline-flex;flex:none;align-items:center;gap:2px} -+.bt-face-marks>.bt-mark{flex:none} -+.bt-face-person{cursor:default} -+.bt-face-person:hover .bt-row-title{text-decoration:none} -+.bt-face-person-text{display:flex;flex-direction:column;min-width:0} -+.bt-face-bots{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:16px;color:var(--bt-ink-3)} -+.bt-face-bot{padding:0;border:0;background:none;color:inherit;font:inherit} -+button.bt-face-bot{cursor:pointer} -+button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-underline-offset:2px} -+.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} -+.bt-bs-stack .bt-seg{align-self:stretch} -+@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} - `; - -+// src/client/palette.js -+var import_react31 = require("react"); -+var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); -+var paletteText = (raw, names) => readableLine(String(raw ?? ""), names); -+var PALETTE_EXIT_MS = 160; -+function CommandPalette({ start, token, roster, actions, useSessions }) { -+ const [query, setQuery] = (0, import_react31.useState)(start ?? ""); -+ const [cursor, setCursor] = (0, import_react31.useState)(0); -+ const [leaving, setLeaving] = (0, import_react31.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 names = botNames(roster); -+ 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 text2 = paletteText(raw, names); -+ const at = text2.toLowerCase().indexOf(wanted); -+ if (at < 0) continue; -+ const from = Math.max(0, at - 24); -+ hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.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 people2 = [ -+ ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ -+ key: bot.id, -+ avatar: (0, import_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), -+ title: pickerBotLabel(bot), -+ 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_react31.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -+ title: room2.name, -+ subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), -+ badge: room2.hidden ? t("Hidden") : void 0, -+ run: async () => { -+ if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); -+ actions.openSession(room2.id); -+ } -+ })) -+ ]; -+ if (people2.length) sections.push([t("Bots"), people2]); -+ const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ -+ key: agent.id, -+ avatar: (0, import_react31.createElement)("span", { className: "bt-palette-agent" }, (0, import_react31.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_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react31.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_react31.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -+ { key: "cmd:bot", icon: (0, import_react31.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -+ { key: "cmd:group", icon: (0, import_react31.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -+ { key: "cmd:settings", icon: (0, import_react31.createElement)(GearIcon), title: t("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_react31.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(); -+ }; -+ let index = -1; -+ return (0, import_react31.createElement)( -+ "div", -+ { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react31.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -+ (0, import_react31.createElement)( -+ "div", -+ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -+ (0, import_react31.createElement)( -+ "div", -+ { className: "bt-palette-head" }, -+ (0, import_react31.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)), -+ (0, import_react31.createElement)("input", { -+ 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_react31.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { -+ setQuery(""); -+ setCursor(0); -+ } }, (0, import_react31.createElement)(CloseIcon)) : null -+ ), -+ (0, import_react31.createElement)( -+ "div", -+ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, -+ (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ rows.length === 0 ? (0, import_react31.createElement)( -+ "div", -+ { className: "bt-palette-empty" }, -+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)), -+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -+ ) : sections.map(([title, items]) => (0, import_react31.createElement)( -+ import_react31.Fragment, -+ { key: title }, -+ (0, import_react31.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), -+ items.map((row) => { -+ index += 1; -+ const at = index; -+ return (0, import_react31.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_react31.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react31.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-palette-text" }, -+ (0, import_react31.createElement)("span", { className: "bt-palette-title" }, row.title), -+ row.subtitle ? (0, import_react31.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null -+ ), -+ row.badge ? (0, import_react31.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null -+ ); -+ }) -+ )) -+ ) -+ ) -+ ); -+} -+ - // src/client/bot-draft.js - function freshBotName(roster, base = "New Bot") { - const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); -@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { - } - function startModel(roster) { - const models = roster.models ?? []; -- return (models.find((model) => model.ref === roster.defaultModel) ?? models[0])?.ref ?? ""; -+ const served = (ref) => ref ? models.find((model) => model.ref === ref) : void 0; -+ const main = roster.byId?.[roster.mainBotId] ?? (roster.bots ?? []).find((bot) => bot.id === roster.mainBotId); -+ return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? ""; - } --function createPayload({ name, look, image, model, effort, brief }) { -+function createPayload({ name, look, image, model, effort }) { - const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } }; - if (model) payload.model = model; - if (effort) payload.effort = effort; -- if (brief) payload.brief = brief; - return payload; - } -+async function afterCreate({ id, text: text2, actions }) { -+ actions.openSession(id); -+ actions.showDetails(id, "settings"); -+ const line2 = String(text2 ?? "").trim(); -+ if (line2 === "") return; -+ try { -+ await actions.send(id, line2); -+ } catch (failure) { -+ actions.notify?.(failure?.message ?? String(failure)); -+ } -+} - - // src/client/create-bot.js --var import_react19 = require("react"); -+var import_react32 = require("react"); - var pick = (list) => list[Math.floor(Math.random() * list.length)]; - var randomLook = (avoid = {}) => { - const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); - const colors = DEFAULT_COLORS.filter((color) => color !== avoid.color); - return { shape: pick(shapes), color: pick(colors) }; - }; --function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCreated, inline = false }) { -- const [look, setLook] = (0, import_react19.useState)(() => randomLook()); -- const [image, setImage] = (0, import_react19.useState)(null); -- const [name, setName] = (0, import_react19.useState)(start?.name ?? ""); -- const [model, setModel] = (0, import_react19.useState)(() => startModel(roster)); -- const [effort, setEffort] = (0, import_react19.useState)(void 0); -- const [busy, setBusy] = (0, import_react19.useState)(false); -- const [error, setError] = (0, import_react19.useState)(""); -- const fileRef = (0, import_react19.useRef)(null); -- const frame = (0, import_react19.useRef)(null); -+function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { -+ const [look, setLook] = (0, import_react32.useState)(() => randomLook()); -+ const [image, setImage] = (0, import_react32.useState)(null); -+ const [name, setName] = (0, import_react32.useState)(start?.name ?? ""); -+ const [model, setModel] = (0, import_react32.useState)(() => startModel(roster)); -+ const [effort, setEffort] = (0, import_react32.useState)(void 0); -+ const [busy, setBusy] = (0, import_react32.useState)(false); -+ const [error, setError] = (0, import_react32.useState)(""); -+ const fileRef = (0, import_react32.useRef)(null); -+ const frame = (0, import_react32.useRef)(null); - useEscape(onBack); -- (0, import_react19.useEffect)(() => { -+ (0, import_react32.useEffect)(() => { - if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); - }, []); - const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr - setError(""); - setBusy(true); - try { -- const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief })); -- onCreated(bot); -+ const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort })); -+ await onCreated(bot); - } catch (failure) { - setError(failure?.message ?? String(failure)); - setBusy(false); -@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr - avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); - }; - const previewLook = image ? { ...look, image } : look; -- const card = (0, import_react19.createElement)( -+ const card = (0, import_react32.createElement)( - "div", - { className: "bt-create-card" }, -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "div", - { className: "bt-create-preview" }, - // The key remounts the mark, so every new pick plays the pop. -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "span", - { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, -- (0, import_react19.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) -+ (0, import_react32.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) - ), -- (0, import_react19.createElement)("span", { className: "bt-create-name" }, finalName) -+ (0, import_react32.createElement)("span", { className: "bt-create-name" }, finalName) - ), -- (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")), -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)("div", { className: "bt-create-step" }, t("Look")), -+ (0, import_react32.createElement)( - "div", - { className: "bt-create-looks" }, -- (0, import_react19.createElement)(CharacterGrid, { -+ (0, import_react32.createElement)(CharacterGrid, { - look, - hasPhoto: Boolean(image), - onShape: (shape) => { -@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr - setLook((current) => ({ ...current, color })); - } - }), -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "div", - { className: "bt-actions bt-create-lookbar" }, -- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { - setImage(null); - setLook((current) => randomLook(current)); - } }, t("Shuffle")), -- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), -- image ? (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, -- (0, import_react19.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), -+ image ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, -+ (0, import_react32.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { - const file = event.target.files?.[0]; - event.target.value = ""; - upload(file); - } }) - ) - ), -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "label", - { className: "bt-field" }, -- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")), -- (0, import_react19.createElement)("input", { -+ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Name")), -+ (0, import_react32.createElement)("input", { - className: "bt-input", - autoFocus: !inline, - value: name, -@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr - } - } - }), -- problem ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null -+ problem ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null - ), -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "div", - { className: "bt-field" }, -- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Model")), -- (0, import_react19.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { -+ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Model")), -+ (0, import_react32.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { - setModel(ref); - setEffort(void 0); - }, onPickEffort: setEffort }), -- (0, import_react19.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) -+ (0, import_react32.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) - ), -- error ? (0, import_react19.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -- (0, import_react19.createElement)( -+ error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ (0, import_react32.createElement)( - "div", - { className: "bt-create-foot" }, -- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")), -- (0, import_react19.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) -+ // The dialog's header already has Back and Close (#230); only the inline form needs its own. -+ inline ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, -+ (0, import_react32.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) - ) - ); -- if (inline) return (0, import_react19.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); -- return (0, import_react19.createElement)( -+ if (inline) return (0, import_react32.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); -+ return (0, import_react32.createElement)( - "div", - { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, -- (0, import_react19.createElement)( -+ (0, import_react32.createElement)( - "div", - { className: "bt-create-head" }, -- canGoBack ? (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react19.createElement)(ChevronLeftIcon)) : (0, import_react19.createElement)("span", { className: "bt-create-gap" }), -- (0, import_react19.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), -- (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react19.createElement)(CloseIcon)) -+ canGoBack ? (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.createElement)("span", { className: "bt-create-gap" }), -+ (0, import_react32.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), -+ (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react32.createElement)(CloseIcon)) - ), -- (0, import_react19.createElement)("div", { className: "bt-create-body" }, card) -+ (0, import_react32.createElement)("div", { className: "bt-create-body" }, card) +-// 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(); +- }; +- 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 +- ); +- }) +- )) +- ) +- ) +- ); +-} +- + // src/client/model-menu.js +-var import_react13 = require("react"); +-var import_react_dom6 = require("react-dom"); ++var import_react22 = 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_react22.useState)(void 0); ++ (0, import_react22.useEffect)(() => { + setFetched(void 0); + if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; + let live = true; +@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { + ]; + } + function EffortRows({ reasoning, current, onPick }) { +- return (0, import_react13.createElement)( +- import_react13.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)( ++ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), ++ effortRows(reasoning).map((row) => (0, import_react22.createElement)( + "button", + { + key: row.effort ?? "default", +@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { + "aria-checked": row.effort === current, + onClick: () => onPick(row.effort) + }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) ++ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, row.label), ++ (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react22.createElement)(CheckIcon) : null) + )) ); } - - // src/client/new-chat.js --var import_react20 = require("react"); --var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives"); -+var import_react33 = require("react"); - function NewChat({ mode, roster, actions }) { -- const [query, setQuery] = (0, import_react20.useState)(""); -- const [picked, setPicked] = (0, import_react20.useState)([]); -- const [group, setGroup] = (0, import_react20.useState)(mode === "group"); -- const [admin, setAdmin] = (0, import_react20.useState)(null); -- const [message, setMessage] = (0, import_react20.useState)(""); -- const [busy, setBusy] = (0, import_react20.useState)(false); -- const [error, setError] = (0, import_react20.useState)(""); -- const [cursor, setCursor] = (0, import_react20.useState)(0); -- const [draft, setDraft] = (0, import_react20.useState)(mode === "create" ? { name: "", fromList: false } : null); -- const inputRef = (0, import_react20.useRef)(null); -+ const [query, setQuery] = (0, import_react33.useState)(""); -+ const [picked, setPicked] = (0, import_react33.useState)([]); -+ const [group, setGroup] = (0, import_react33.useState)(mode === "group"); -+ const [compose, setCompose] = (0, import_react33.useState)(mode !== "new"); -+ const [admin, setAdmin] = (0, import_react33.useState)(null); -+ const [message, setMessage] = (0, import_react33.useState)(""); -+ const [busy, setBusy] = (0, import_react33.useState)(false); -+ const [error, setError] = (0, import_react33.useState)(""); -+ const [fix, setFix] = (0, import_react33.useState)(null); -+ const [fixNote, setFixNote] = (0, import_react33.useState)(""); -+ const [inviteLink, setInviteLink] = (0, import_react33.useState)(""); -+ const [cursor, setCursor] = (0, import_react33.useState)(0); -+ const [draft, setDraft] = (0, import_react33.useState)(mode === "create" ? { name: "", fromList: false } : null); -+ const inputRef = (0, import_react33.useRef)(null); - const close = () => actions.closeOverlay(); - useEscape(close); - const paneLeft = usePaneLeft(); - const wanted = query.trim().toLowerCase(); -- const matches = roster.bots.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); -+ const bots = listedChatBots(roster); - const choosingAdmin = group && admin === null; -- const exact = roster.bots.some((bot) => bot.name.toLowerCase() === wanted); -+ const pool = choosingAdmin ? bots.filter((bot) => !bot.remote) : bots; -+ const matches = pool.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || pickerBotLabel(bot).toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); -+ const exact = bots.some((bot) => bot.name.toLowerCase() === wanted); - const pickedBots = picked.map((id) => roster.byId[id]).filter(Boolean); - const run = async (task) => { - if (busy) return; - setError(""); -+ setFix(null); -+ setFixNote(""); -+ setInviteLink(""); - setBusy(true); - try { - await task(); - } catch (failure) { - setError(failure?.message ?? String(failure)); -+ setFix(ungrantedLocalFix(failure?.details) ?? null); - } finally { - setBusy(false); - } - }; -+ const leaveOut = (id) => { -+ if (id === admin) setAdmin(null); -+ setPicked((current) => current.filter((other) => other !== id)); -+ setError(""); -+ setFix(null); -+ setFixNote(""); -+ setInviteLink(""); -+ }; -+ const copyInvite = async (bot) => { -+ const social = globalThis.__flybot__?.social; -+ let link; -+ try { -+ const invite = await social.createInvite(bot.id); -+ link = invite?.link || invite?.url; -+ if (!link) throw new Error("no link"); -+ } catch { -+ setInviteLink(""); -+ setFixNote(t("Could not create the invite. Use Bot settings → Sharing → Copy invite link for this Bot.")); -+ return; -+ } -+ if (await copiedToClipboard(link)) { -+ setInviteLink(""); -+ setFixNote(t("Invite link for {name} copied. Send it to your friend; once they accept, create the group again.", { name: bot.name })); -+ } else { -+ setInviteLink(link); -+ setFixNote(t("Could not copy the invite link for {name}. Copy the link below and send it to your friend; once they accept, create the group again.", { name: bot.name })); -+ } -+ }; - const pick2 = (bot) => { - setPicked((current) => [...current, bot.id]); - setQuery(""); -@@ -8386,71 +8386,104 @@ function NewChat({ mode, roster, actions }) { - }; - const created = (bot) => { - close(); -- actions.openSession(bot.id); -- actions.showDetails(bot.id, "settings"); -+ return afterCreate({ id: bot.id, text: message, actions }); - }; - const makeGroup = async () => { - if (admin === null || picked.length === 0) return; -- const text = message.trim(); -- const room2 = await actions.createRoom([admin, ...picked], { admin, ...text ? {} : { greet: true } }); -- if (text) await actions.send(room2.id, text); -+ const text2 = message.trim(); -+ const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); -+ const participants = remotes.length === 0 ? void 0 : [ -+ ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), -+ ...remotes -+ ]; -+ const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); -+ if (text2) await actions.send(room2.id, text2); - actions.openSession(room2.id); - close(); - }; - const createGroup = () => run(makeGroup); - const sendMessage = () => run(async () => { -- const text = message.trim(); -+ const text2 = message.trim(); - if (group && admin !== null) { - await makeGroup(); - return; - } -- if (text === "" || picked.length === 0) return; -+ if (text2 === "" || picked.length === 0) return; - if (picked.length === 1) { -- await actions.send(picked[0], text); -+ await actions.send(picked[0], text2); - actions.openSession(picked[0]); - close(); - return; - } -- const room2 = await actions.createRoom(picked); -- await actions.send(room2.id, text); -+ const { members, remotes } = splitRoomPicks(roster, picked); -+ const participants = remotes.length === 0 ? void 0 : [ -+ ...members.map((id) => ({ kind: "bot", id })), -+ ...remotes -+ ]; -+ const room2 = await actions.createRoom(members, participants ? { participants } : {}); -+ await actions.send(room2.id, text2); - actions.openSession(room2.id); - close(); - }); -+ const menuOnly = mode === "new" && !compose && !group && picked.length === 0 && wanted === ""; - const options = []; - if (!group && picked.length === 0 && mode !== "search" && wanted === "") { -- options.push({ key: "new", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot"), run: createBot }); -- options.push({ key: "group", icon: (0, import_react20.createElement)(PeopleIcon), label: t("Create group chat"), run: () => { -+ if (menuOnly) { -+ options.push({ key: "chat", icon: (0, import_react33.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { -+ setCompose(true); -+ setCursor(0); -+ setTimeout(() => inputRef.current?.focus()); -+ } }); -+ } -+ options.push({ key: "new", icon: (0, import_react33.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); -+ options.push({ key: "group", icon: (0, import_react33.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { - setGroup(true); -+ setCompose(true); - setCursor(0); - inputRef.current?.focus(); - } }); -+ for (const action of newChatActionRows({ mode, group, picked, query, roster })) { -+ options.push({ -+ key: action.key, -+ icon: action.icon ? (0, import_react33.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react33.createElement)(PeopleIcon) }) : (0, import_react33.createElement)(PeopleIcon), -+ label: t(action.label), -+ hint: action.hint ? t(action.hint) : void 0, -+ run: () => { -+ const done = Promise.resolve().then(() => action.run({ close, actions, roster })); -+ done.then((note) => { -+ if (typeof note === "string" && note) actions.notify?.(note); -+ }).catch((failure) => setError(failure?.message ?? String(failure))); -+ } -+ }); -+ } - if ((roster.agents ?? []).length === 0) { -- options.push({ key: "agent", icon: (0, import_react20.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add DSH Agent"), run: () => run(async () => { -+ options.push({ key: "agent", icon: (0, import_react33.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { - await actions.addAgent(); - close(); - }) }); - } - } -- for (const bot of matches) { -- options.push({ key: bot.id, bot, label: bot.name, hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); -+ if (!menuOnly) { -+ for (const bot of matches) { -+ options.push({ key: bot.id, bot, label: pickerBotLabel(bot), hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); -+ } - } -- if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot “{name}”", { name: query.trim() }), run: createBot }); -+ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react33.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); - const active = Math.min(cursor, Math.max(options.length - 1, 0)); - const listed = options.map((option) => option.key).join("\n"); - const glide = useGlide(`${active} - ${listed}`, { axis: "y", ...QUICK_EDGES }); - useFlip(glide.box, listed, "[data-flip]"); -- const drop = (0, import_react20.useRef)(null); -+ const drop = (0, import_react33.useRef)(null); - useMorph(drop, listed); - if (draft) { -- return (0, import_react20.createElement)( -+ return (0, import_react33.createElement)( + 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_react22.createElement)( ++ import_react22.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)( ++ (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), ++ list.map((model) => (0, import_react22.createElement)( + "button", + { + key: model.ref, +@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { + title: model.ref, + onClick: () => onPick(model.ref) + }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), +- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) ++ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, model.name), ++ model.ref === roster.defaultModel ? (0, import_react22.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, ++ (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react22.createElement)(CheckIcon) : null) + )) + )); + } +-var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); ++var Caret = () => (0, import_react22.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_react22.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); + function useAnchoredMenu() { +- const [place, setPlace] = (0, import_react13.useState)(null); ++ const [place, setPlace] = (0, import_react22.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 button = (0, import_react22.useRef)(null); ++ const menu = (0, import_react22.useRef)(null); + const close = () => setPlace(null); +- (0, import_react13.useEffect)(() => { ++ (0, import_react22.useEffect)(() => { + if (place === null) return void 0; + const onDown = (event) => { + if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; +@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { + return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; + } + function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { ++ if (bot?.remote !== void 0) return null; + const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); + const shown = modelOf(bot, roster); + const current = bot.model ?? bot.appliedModel; +@@ -6511,10 +6511,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo + else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); + }; + const models = roster.models ?? []; +- return (0, import_react13.createElement)( +- import_react13.Fragment, ++ return (0, import_react22.createElement)( ++ import_react22.Fragment, + null, +- (0, import_react13.createElement)( ++ (0, import_react22.createElement)( + "button", + { + ref: button, +@@ -6527,37 +6527,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo + "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, + onClick: toggle + }, +- (0, import_react13.createElement)( ++ (0, import_react22.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_react22.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), ++ shown ? (0, import_react22.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_react22.createElement)(Caret) + ), +- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( ++ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react22.createElement)( "div", - { className: "bt-pane", style: { left: paneLeft } }, -- (0, import_react20.createElement)(CreateBot, { -+ (0, import_react33.createElement)(CreateBot, { - roster, - actions, - start: draft, -- brief: message.trim(), - canGoBack: draft.fromList, - onBack: draft.fromList ? () => { - setDraft(null); -@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); - if (picked.length > 0) setPicked((current) => current.slice(0, -1)); - else if (admin !== null) setAdmin(null); + { 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_react22.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react22.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), ++ effortRows(reasoning).length > 0 ? (0, import_react22.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, ++ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react22.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_react22.createElement)(SlidersIcon), (0, import_react22.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_react22.useState)(""); ++ const [error, setError] = (0, import_react22.useState)(""); + const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; +- (0, import_react13.useEffect)(() => { ++ (0, import_react22.useEffect)(() => { + 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_react22.createElement)(RemoteGoneDock, { bot }) : null; + const models = roster.models ?? []; + const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; + if (bot.model && (!lost || models.length === 0)) return null; +@@ -6569,13 +6569,13 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { + void actions.refreshModels?.(); + }).finally(() => setBusy("")); }; -- return (0, import_react20.createElement)( -+ return (0, import_react33.createElement)( +- 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_react22.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react22.createElement)(KeyHelpLink, { key: "key-help" })] : [ ++ ...groupModels(models).map(([provider, list]) => (0, import_react22.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", { ++ (0, import_react22.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), ++ (0, import_react22.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react22.createElement)("button", { + key: model.ref, + type: "button", + className: "bt-model-chip", +@@ -6584,50 +6584,115 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { + "aria-label": `${model.name} · ${provider}`, + title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, + onClick: () => pick2(model.ref) +- }, 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_react22.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_react22.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react22.createElement)(SlidersIcon), t("Manage models")) + ]; +- return (0, import_react13.createElement)( ++ return (0, import_react22.createElement)( "div", -- { className: "bt-pane", style: { left: paneLeft } }, -- (0, import_react20.createElement)( -+ { -+ className: "bt-pane", -+ style: { left: paneLeft }, -+ "data-menu": menuOnly || void 0, -+ onMouseDown: menuOnly ? (event) => { -+ if (event.target === event.currentTarget) close(); -+ } : void 0 -+ }, -+ (0, import_react33.createElement)( + { className: "bt-model-dock" }, +- (0, import_react13.createElement)( ++ (0, import_react22.createElement)( "div", -- { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : t("New chat") }, -- (0, import_react20.createElement)( -+ { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, -+ // The popover needs no title row: Escape or a press outside closes it. -+ menuOnly ? null : (0, import_react33.createElement)( - "div", - { className: "bt-to" }, -- (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")), -- adminBot ? (0, import_react20.createElement)( -+ (0, import_react33.createElement)("span", { className: "bt-to-label" }, t("To:")), -+ adminBot ? (0, import_react33.createElement)( - "span", - { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, -- (0, import_react20.createElement)(BotMark, { bot: adminBot, size: 16 }), -+ (0, import_react33.createElement)(BotMark, { bot: adminBot, size: 16 }), - adminBot.name, -- (0, import_react20.createElement)(AdminStar), -- (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") -+ (0, import_react33.createElement)(AdminStar), -+ (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") - ) : null, -- choosingAdmin ? (0, import_react20.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react20.createElement)(AdminStar)) : null, -- pickedBots.map((bot) => (0, import_react20.createElement)( -+ choosingAdmin ? (0, import_react33.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react33.createElement)(AdminStar)) : null, -+ pickedBots.map((bot) => (0, import_react33.createElement)( - "span", - { key: bot.id, className: "bt-token" }, -- (0, import_react20.createElement)(BotMark, { bot, size: 16 }), -+ (0, import_react33.createElement)(BotMark, { bot, size: 16 }), - bot.name, -- (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") -+ (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") - )), -- (0, import_react20.createElement)("input", { -+ (0, import_react33.createElement)("input", { - ref: inputRef, - autoFocus: true, - value: query, -@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); - } else if (event.key === "Backspace" && query === "") unpick(); - } - }), -- group && admin !== null ? (0, import_react20.createElement)("button", { -+ group && admin !== null ? (0, import_react33.createElement)("button", { - type: "button", - className: "bt-send bt-to-create", - disabled: busy || picked.length === 0, - title: picked.length === 0 ? t("Add at least one more Bot") : void 0, - onClick: createGroup - }, t("Create group chat")) : null, -- (0, import_react20.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") - ), -- (0, import_react20.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react20.createElement)( -+ (0, import_react33.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react33.createElement)( - "div", - { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, -- (0, import_react20.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- options.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react20.createElement)( -+ (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react33.createElement)( - "button", - { - key: option.key, -@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); - onMouseEnter: () => setCursor(index), - onClick: option.run - }, -- option.bot ? (0, import_react20.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react20.createElement)("span", { className: "bt-option-icon" }, option.icon), -- (0, import_react20.createElement)("span", { className: "bt-option-label" }, option.label), -- option.hint && index === active ? (0, import_react20.createElement)("span", { className: "bt-option-hint" }, option.hint) : null -+ option.bot ? (0, import_react33.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react33.createElement)("span", { className: "bt-option-icon" }, option.icon), -+ // An action row's description sits under its title and is always there (#283): a long line -+ // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. -+ // A Bot row's short hint stays beside the name and shows on the active row only. -+ option.bot || !option.hint ? (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react33.createElement)("span", { className: "bt-option-copy" }, (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react33.createElement)("span", { className: "bt-option-sub" }, option.hint)), -+ option.bot && option.hint && index === active ? (0, import_react33.createElement)("span", { className: "bt-option-hint" }, option.hint) : null - )) - )), -- (0, import_react20.createElement)("div", { className: "bt-nc-spacer" }), -- error ? (0, import_react20.createElement)("div", { className: "bt-error" }, error) : null, -- (0, import_react20.createElement)( -+ menuOnly ? null : (0, import_react33.createElement)("div", { className: "bt-nc-spacer" }), -+ error && fix ? (0, import_react33.createElement)( -+ "div", -+ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, -+ (0, import_react33.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), -+ (0, import_react33.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ -+ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react33.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { -+ void copyInvite(bot); -+ } }, t("Copy invite link for {name}", { name: bot.name })) : null, -+ (0, import_react33.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) -+ ])), -+ fixNote ? (0, import_react33.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, -+ inviteLink ? (0, import_react33.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null -+ ) : error ? (0, import_react33.createElement)("div", { className: "bt-error" }, error) : null, -+ menuOnly ? null : (0, import_react33.createElement)( + { className: "bt-astack", "data-lead": "" }, +- (0, import_react13.createElement)( ++ (0, import_react22.createElement)( "div", - { className: "bt-nc-compose" }, -- (0, import_react20.createElement)( -+ (0, import_react33.createElement)( + { className: "bt-msg" }, +- (0, import_react13.createElement)( ++ (0, import_react22.createElement)( "div", - { className: "bt-nc-card" }, -- (0, import_react20.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react20.createElement)(PlusIcon)), -- (0, import_react20.createElement)("textarea", { -+ (0, import_react33.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react33.createElement)(PlusIcon)), -+ (0, import_react33.createElement)("textarea", { - rows: 1, - value: message, - placeholder, -@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); - } - } - }), -- (0, import_react20.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react20.createElement)(ArrowUpIcon)) -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react33.createElement)(ArrowUpIcon)) + { className: "bt-msg-line" }, +- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) ++ (0, import_react22.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react22.createElement)("div", { className: "bt-bubble" }, text2) ) ) - ) -@@ -8577,18 +8577,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + ), +- (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 ++ (0, import_react22.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), ++ error ? (0, import_react22.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null ++ ); ++} ++function RemoteGoneDock({ bot }) { ++ return (0, import_react22.createElement)( ++ "div", ++ { className: "bt-model-dock", "data-flybot": "remote-gone" }, ++ (0, import_react22.createElement)( ++ "div", ++ { className: "bt-astack", "data-lead": "" }, ++ (0, import_react22.createElement)( ++ "div", ++ { className: "bt-msg" }, ++ (0, import_react22.createElement)( ++ "div", ++ { className: "bt-msg-line" }, ++ (0, import_react22.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react22.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 })) ++ ) ++ ) ++ ) + ); + } + 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] ++ [t("Open settings to set up a custom model"), openHarnessModels] + ].filter(Boolean); + } ++var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; ++function KeyHelpLink() { ++ return (0, import_react22.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); ++} ++function missingModelKey(message) { ++ const text2 = String(message ?? ""); ++ if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; ++ return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; ++} ++function modelKeyChoices(actions, route) { ++ if (/^deepseek/i.test(route)) return setupChoices(actions); ++ return [[t("Manage models"), openHarnessModels]]; ++} ++function TurnErrorCell({ node, t: chatT, actions }) { ++ const data = node?.data ?? {}; ++ const code = typeof data.code === "string" && data.code ? data.code : void 0; ++ const raw = String(data.message ?? ""); ++ const shell = (key, fallback) => { ++ const text2 = typeof chatT === "function" ? chatT(key) : void 0; ++ return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; ++ }; ++ const missing = missingModelKey(raw); ++ const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); ++ const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; ++ const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; ++ const deepseek = missing ? /^deepseek/i.test(missing.route) : false; ++ return (0, import_react22.createElement)( ++ "div", ++ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, ++ (0, import_react22.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), ++ (0, import_react22.createElement)( ++ "div", ++ { className: "bt-turn-error-copy" }, ++ (0, import_react22.createElement)("span", { className: "bt-turn-error-title" }, title), ++ (0, import_react22.createElement)("span", { className: "bt-turn-error-message" }, message), ++ code ? (0, import_react22.createElement)("code", { className: "bt-turn-error-code" }, code) : null ++ ), ++ choices.length > 0 ? (0, import_react22.createElement)( ++ "div", ++ { className: "bt-turn-error-actions" }, ++ choices.map(([label, run]) => (0, import_react22.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), ++ deepseek ? (0, import_react22.createElement)(KeyHelpLink, { key: "key-help" }) : null ++ ) : null ++ ); ++} + 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 }); ++ return (0, import_react22.createElement)(SignInHost, { key: open, entry, actions }); + } + function SignInHost({ entry, actions }) { +- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); ++ const [ops] = (0, import_react22.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, { ++ const useHook = (source) => (selector) => (0, import_react22.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); ++ const [hooks] = (0, import_react22.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); ++ return (0, import_react22.createElement)(entry.component, { + ...ops, + ...hooks, + t: actions.accountT(), +@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) { + }); } + 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} ++.bt-turn-error-copy{min-width:0;overflow-wrap:anywhere} ++.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135} ++.bt-turn-error-message{color:var(--bt-ink-2)} ++.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} ++.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px} ++.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px} ++.bt-model-help:hover{color:var(--bt-ink)} ++.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px} + .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} + .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} + .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} +@@ -6675,18 +6675,96 @@ var MODEL_CSS = ` + @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} + `; - // src/client/exchange.js --var import_react21 = require("react"); -+var import_react34 = require("react"); - var SHEET_EXIT_MS = 220; -+function exchangeText(text2) { -+ if (typeof text2 !== "string") return ""; -+ const CLOSE = ""; -+ let body = text2; -+ for (let previous; previous !== body; ) { -+ previous = body; -+ const open = body.indexOf(" open ? body.slice(body.indexOf(">", open) + 1, close) : body; -+ body = stripFence(body.slice(0, open) + inner + (close > open ? body.slice(close + CLOSE.length) : "")); ++// src/client/model-test.js ++var import_react23 = require("react"); ++var FAILURE_COPY = { ++ NO_MODEL: "Pick a model first.", ++ NO_KEY: "This model has no API key yet. Set one up in the model settings.", ++ AUTH: "The API key was rejected. Check it in the model settings.", ++ QUOTA: "The account is out of quota.", ++ RATE_LIMIT: "Too many requests right now. Try again in a moment.", ++ TIMEOUT: "The model did not answer within 30 seconds.", ++ NOT_SERVED: "This model is not available now. Check its provider in the model settings.", ++ MODEL_NOT_FOUND: "The provider does not know this model. Check the model name.", ++ TRANSPORT: "Could not reach the model service. Check the endpoint address and your network.", ++ SERVER: "The model service failed. Try again later.", ++ INVALID_REQUEST: "The model service refused the request. Check the endpoint and the model.", ++ UNKNOWN: "The test failed." ++}; ++function testOutcome(result, modelName) { ++ if (result?.ok === true) { ++ const seconds = typeof result.ms === "number" ? Math.max(0.1, Math.round(result.ms / 100) / 10) : void 0; ++ return { ok: true, text: seconds === void 0 ? t("Works. {model} answered.", { model: modelName }) : t("Works. {model} answered in {seconds} s.", { model: modelName, seconds }) }; + } -+ return stripFence(stripTrace(body.replace(/^\s+/, ""))).trim(); ++ const code = Object.hasOwn(FAILURE_COPY, result?.code) ? result.code : "UNKNOWN"; ++ return { ok: false, code, text: t(FAILURE_COPY[code]) }; +} -+function ExchangeLine({ entry, roster, actions }) { -+ const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; -+ return (0, import_react34.createElement)( ++var testModel = (id) => call("test-model", { id }); ++function ModelTestRow({ bot, modelName, test = testModel }) { ++ const [state, setState] = (0, import_react23.useState)({ busy: false, outcome: void 0 }); ++ const live = (0, import_react23.useRef)(true); ++ (0, import_react23.useEffect)(() => () => { ++ live.current = false; ++ }, []); ++ (0, import_react23.useEffect)(() => { ++ setState({ busy: false, outcome: void 0 }); ++ }, [bot.id, bot.model]); ++ const start = () => { ++ if (state.busy) return; ++ setState({ busy: true, outcome: void 0 }); ++ Promise.resolve().then(() => test(bot.id)).then( ++ (result) => testOutcome(result, modelName ?? result?.ref ?? ""), ++ (error) => ({ ok: false, text: String(error?.message ?? t("The test failed.")) }) ++ ).then((outcome2) => { ++ if (live.current) setState({ busy: false, outcome: outcome2 }); ++ }); ++ }; ++ const { busy, outcome } = state; ++ return (0, import_react23.createElement)( + "div", -+ { className: "bt-group-msg" }, -+ (0, import_react34.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), -+ (0, import_react34.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react34.createElement)("span", { className: "bt-lead" }, (0, import_react34.createElement)(BotMark, { bot, size: 22 })) }) ++ { className: "bt-set-row bt-bs-row bt-model-test" }, ++ (0, import_react23.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), ++ (0, import_react23.createElement)( ++ "span", ++ { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, ++ busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, ++ outcome?.code ? (0, import_react23.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null ++ ) ++ ), ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) + ); +} - function ExchangeDialog({ pair, roster, actions }) { -- const [entries2, setEntries] = (0, import_react21.useState)(null); -- const [leaving, setLeaving] = (0, import_react21.useState)(false); -+ const [entries2, setEntries] = (0, import_react34.useState)(null); -+ const [leaving, setLeaving] = (0, import_react34.useState)(false); - const close = () => { - if (leaving) return; - setLeaving(true); - setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS); - }; - useEscape(close); -- (0, import_react21.useEffect)(() => { -+ (0, import_react34.useEffect)(() => { - let stop = false; - let timer; - let seen = null; -@@ -8612,49 +8612,42 @@ function ExchangeDialog({ pair, roster, actions }) { - const right = roster.byId[pair[1]]; - let lastDay = 0; - const paneLeft = usePaneLeft(); -- return (0, import_react21.createElement)( -+ return (0, import_react34.createElement)( - "div", - { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, -- (0, import_react21.createElement)( -+ (0, import_react34.createElement)( - "div", - { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, -- (0, import_react21.createElement)("div", { className: "bt-head" }, (0, import_react21.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-head" }, (0, import_react34.createElement)( - "span", - { className: "bt-pill", style: { cursor: "default" } }, -- left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null, -+ left ? (0, import_react34.createElement)(BotMark, { bot: left, size: 22 }) : null, - left?.name ?? "?", -- (0, import_react21.createElement)("span", { className: "bt-swap" }, "⇄"), -- right ? (0, import_react21.createElement)(BotMark, { bot: right, size: 22 }) : null, -+ (0, import_react34.createElement)("span", { className: "bt-swap" }, "⇄"), -+ right ? (0, import_react34.createElement)(BotMark, { bot: right, size: 22 }) : null, - right?.name ?? "?" - )), -- (0, import_react21.createElement)( -+ (0, import_react34.createElement)( - "div", - { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, -- entries2 === null ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { -- const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; -+ entries2 === null ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { - const showDay = entry.time - lastDay > 30 * 60 * 1e3; - lastDay = entry.time; -- return (0, import_react21.createElement)( -- import_react21.Fragment, -+ return (0, import_react34.createElement)( -+ import_react34.Fragment, - { key: entry.id }, -- showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, -- (0, import_react21.createElement)( -- "div", -- { className: "bt-group-msg" }, -- (0, import_react21.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), -- (0, import_react21.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react21.createElement)("span", { className: "bt-lead" }, (0, import_react21.createElement)(BotMark, { bot, size: 22 })) }) -- ) -+ showDay ? (0, import_react34.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, -+ (0, import_react34.createElement)(ExchangeLine, { entry, roster, actions }) - ); - }) - ), -- (0, import_react21.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) -+ (0, import_react34.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react34.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) - ) - ); - } - - // src/client/voice.js --var import_react22 = require("react"); --var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); -+var import_react35 = require("react"); - var VOICE_LABELS = { - listening: "Listening", - thinking: "Thinking", -@@ -8665,14 +8665,14 @@ var VOICE_LABELS = { - }; - var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; - function useMicLevel(enabled) { -- const [level, setLevel] = (0, import_react22.useState)(0); -- (0, import_react22.useEffect)(() => { -+ const [level, setLevel] = (0, import_react35.useState)(0); -+ (0, import_react35.useEffect)(() => { - if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; - let stream = null; - let audio = null; - let frame = 0; - let stopped = false; -- navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { -+ void navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { - if (stopped) { - media.getTracks().forEach((track) => track.stop()); - return; -@@ -8716,18 +8716,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; - return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; - }); -- const [speech] = (0, import_react22.useState)(() => actions.speech()); -- const [phase, setPhase] = (0, import_react22.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); -- const [catalog, setCatalog] = (0, import_react22.useState)(null); -- const [heard, setHeard] = (0, import_react22.useState)(""); -- const [said, setSaid] = (0, import_react22.useState)(""); -- const [note, setNote] = (0, import_react22.useState)(""); -- const [serviceLevel, setServiceLevel] = (0, import_react22.useState)(0); -- const [cycle, setCycle] = (0, import_react22.useState)(0); -- const waiting = (0, import_react22.useRef)(null); -- const reply = (0, import_react22.useRef)(lastReply); -+ const [speech] = (0, import_react35.useState)(() => actions.speech()); -+ const [phase, setPhase] = (0, import_react35.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); -+ const [catalog, setCatalog] = (0, import_react35.useState)(null); -+ const [heard, setHeard] = (0, import_react35.useState)(""); -+ const [said, setSaid] = (0, import_react35.useState)(""); -+ const [note, setNote] = (0, import_react35.useState)(""); -+ const [serviceLevel, setServiceLevel] = (0, import_react35.useState)(0); -+ const [cycle, setCycle] = (0, import_react35.useState)(0); -+ const waiting = (0, import_react35.useRef)(null); -+ const reply = (0, import_react35.useRef)(lastReply); - reply.current = lastReply; -- const turn = (0, import_react22.useRef)(0); -+ const turn = (0, import_react35.useRef)(0); - const paneLeft = usePaneLeft(); - const close = () => { - turn.current += 1; -@@ -8735,24 +8735,24 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - actions.closeVoice(); - }; - useEscape(close); -- const submit = (text) => { -- setHeard(text); -+ const submit = (text2) => { -+ setHeard(text2); - setPhase("thinking"); - waiting.current = reply.current.split("|")[0]; -- void actions.send(sessionId, text.trim()).catch(() => setPhase("listening")); -+ void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); ++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} ++.bt-model-test-result[data-ok=true]{color:#1a7f37} ++.bt-model-test-result[data-ok=false]{color:#c21d2e} ++.bt-model-test-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} ++/* #1347: the hint beside "Test connection" is prose and wraps; the button next to it keeps its own ++ words ("Test connection" on one line) and takes the room it needs. */ ++.bt-model-test>.bt-set-copy{flex:1 1 12ch;min-width:12ch} ++.bt-model-test>.bt-soft{flex:none;white-space:nowrap} ++@container bt-panel (max-width:300px){ ++ .bt-model-test{flex-wrap:wrap;row-gap:6px} ++ .bt-model-test>.bt-set-copy{flex:1 1 100%} ++} ++`; ++ + // src/client/folder-menu.js +-var import_react14 = require("react"); +-var import_react_dom7 = require("react-dom"); ++var import_react24 = require("react"); ++var import_react_dom9 = 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); ++var Tick = ({ on }) => (0, import_react24.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react24.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 [info, setInfo] = (0, import_react24.useState)(null); ++ const [view, setView] = (0, import_react24.useState)("list"); ++ const [draft, setDraft] = (0, import_react24.useState)(""); ++ const [listing, setListing] = (0, import_react24.useState)(null); ++ const [problem, setProblem] = (0, import_react24.useState)(""); + const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); + const toggle = () => { + if (isOpen) { +@@ -6719,35 +6719,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } }; -- const dictation = useDictation((text, final) => { -- setHeard(text); -+ const dictation = useDictation((text2, final) => { -+ setHeard(text2); - if (!final) return; - dictation.stop(); -- submit(text); -+ submit(text2); - }); -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (!note) return void 0; - const timer = setTimeout(() => setNote(""), 2600); - return () => clearTimeout(timer); - }, [note]); -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (!speech) return void 0; - let live = true; - speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - }; - }, [speech]); - const listen = phase === "listening"; -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (speech || !listen || dictation.listening) return void 0; - const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { - } }), 250); - return () => clearTimeout(timer); - }, [listen, dictation.listening]); -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (phase !== "listening") dictation.stop(); - }, [phase]); -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (!speech || !catalog || !listen) return void 0; - const own = turn.current; - let capture = null; -@@ -8789,9 +8789,9 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - setPhase("transcribing"); - try { - const wave = await current.finish(catalog.maxDurationSeconds); -- const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; -+ const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; - if (turn.current !== own) return; -- if (text) submit(text); -+ if (text2) submit(text2); - else setPhase("listening"); - } catch (error) { - if (turn.current !== own) return; -@@ -8838,12 +8838,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - setServiceLevel(0); - }; - }, [speech, catalog, listen, cycle]); -- (0, import_react22.useEffect)(() => { -+ (0, import_react35.useEffect)(() => { - if (phase !== "thinking" || running || waiting.current === null) return; -- const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; -- if (count === waiting.current || !text.trim()) return; -+ const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; -+ if (count === waiting.current || !text2.trim()) return; - waiting.current = null; -- const spoken = plainSpeech(text); -+ const spoken = speechOf(text2, botNames(roster)); - setSaid(spoken); - setHeard(""); - if (!window.speechSynthesis || !spoken) { -@@ -8858,7 +8858,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - window.speechSynthesis.cancel(); - window.speechSynthesis.speak(utterance); - }, [phase, running, lastReply]); -- (0, import_react22.useEffect)(() => () => window.speechSynthesis?.cancel(), []); -+ (0, import_react35.useEffect)(() => () => window.speechSynthesis?.cancel(), []); - const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); - const level = speech ? serviceLevel : micLevel; - const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,291 +8875,678 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - const openSetup = phase === "setup" ? actions.voiceSetup() : null; - const caption = phase === "speaking" ? said : heard; - const shownNote = note || dictation.note; -- return (0, import_react22.createElement)( -+ return (0, import_react35.createElement)( + const known = info?.folders ?? []; + const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; +- const listView = (0, import_react14.createElement)( ++ const listView = (0, import_react24.createElement)( "div", - { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, -- (0, import_react22.createElement)( -+ (0, import_react35.createElement)( - "div", - { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, -- (0, import_react22.createElement)( -+ (0, import_react35.createElement)( - "div", - { className: "bt-voice-orb", style: { "--bt-level": level } }, -- (0, import_react22.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), -- (0, import_react22.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), -- bot ? (0, import_react22.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null -+ (0, import_react35.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), -+ (0, import_react35.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), -+ bot ? (0, import_react35.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null - ), -- (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name), -- (0, import_react22.createElement)( -+ (0, import_react35.createElement)("div", { className: "bt-voice-name" }, name), -+ (0, import_react35.createElement)( - "div", - { className: "bt-voice-status", role: "status" }, -- phase === "thinking" ? (0, import_react22.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react22.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" -+ phase === "thinking" ? (0, import_react35.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react35.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" + { key: "list", className: "bt-folder-view" }, +- (0, import_react14.createElement)( ++ (0, import_react24.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 }) ++ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, fallback), ++ (0, import_react24.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)( ++ folders.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, ++ folders.map((folder) => (0, import_react24.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_react24.createElement)("span", { className: "bt-folder-copy" }, (0, import_react24.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react24.createElement)("span", { className: "bt-folder-path" }, folder.path)), ++ (0, import_react24.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: () => { ++ (0, import_react24.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ info?.picker === "native" ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, ++ info?.picker === "browse" ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, ++ (0, import_react24.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…"))) ++ } }, (0, import_react24.createElement)(PencilIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) + ); +- const typeView = (0, import_react14.createElement)( ++ const typeView = (0, import_react24.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", { ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react24.createElement)(ChevronLeftIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Back"))), ++ (0, import_react24.createElement)("input", { + className: "bt-folder-input", + autoFocus: true, + value: draft, +@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } + 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")) ++ (0, import_react24.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)( ++ const browseView = listing ? (0, import_react24.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)( ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react24.createElement)(ChevronLeftIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Back"))), ++ (0, import_react24.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), ++ parent ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, ++ inside.map((entry) => (0, import_react24.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) ++ (0, import_react24.createElement)(FolderIcon), ++ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, entry.name), ++ (0, import_react24.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")) ++ inside.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, ++ listing.truncated ? (0, import_react24.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, ++ (0, import_react24.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react24.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, ++ return (0, import_react24.createElement)( ++ import_react24.Fragment, + null, +- (0, import_react14.createElement)( ++ (0, import_react24.createElement)( + "button", + { + ref: button, +@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } + title: value, + onClick: toggle + }, +- (0, import_react14.createElement)( ++ (0, import_react24.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_react24.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), ++ value ? (0, import_react24.createElement)("span", { className: "bt-model-provider" }, value) : null ), -- (0, import_react22.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), -- openSetup ? (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { -+ (0, import_react35.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), -+ openSetup ? (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { - close(); - openSetup(); - } }, t("Set up voice input")) : null, -- shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null -+ shownNote ? (0, import_react35.createElement)("div", { className: "bt-voice-note" }, shownNote) : null +- (0, import_react14.createElement)(Caret) ++ (0, import_react24.createElement)(Caret) ), -- (0, import_react22.createElement)( -+ (0, import_react35.createElement)( +- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( ++ shownPlace ? (0, import_react_dom9.createPortal)((0, import_react24.createElement)( "div", - { className: "bt-voice-bar" }, -- (0, import_react22.createElement)("button", { -+ (0, import_react35.createElement)("button", { - type: "button", - className: "bt-voice-btn", - "aria-pressed": phase === "paused", - disabled: blocked, - "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), - onClick: togglePause -- }, (0, import_react22.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react22.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), -- (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react22.createElement)(CloseIcon)) -+ }, (0, import_react35.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react35.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), -+ (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react35.createElement)(CloseIcon)) - ) + { 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 ++ problem ? (0, import_react24.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null + ), document.body) : null ); } - --// src/client/group-settings.js --var import_react23 = require("react"); --var ROOM_MODES = [ -- ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], -- ["everyone", "Everyone", "Every member answers in turn, admin first."] --]; --var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; --function roomSubtitle(room2, roster) { -- const admin = roster.byId[room2.admin]; -- return [t("{count} members", { count: room2.members.length }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); -+// src/client/workflows.js -+var import_react36 = require("react"); -+var READ_MS = 8e3; -+var POLL_MS6 = 2e3; -+var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; -+var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; -+function workflowCallMissing(failure) { -+ const code = failure && typeof failure === "object" ? String(failure.code ?? "") : ""; -+ const message = typeof failure === "string" ? failure : String(failure?.message ?? ""); -+ return code === "bot/not-found" || /Unknown FlyBot endpoint workflow\//i.test(message); -+} -+function workflowCallError(failure, fallback = "Could not read workflows") { -+ if (workflowCallMissing(failure)) return WORKFLOW_PLUGIN_HINT; -+ if (typeof failure === "string" && failure.trim()) return failure.trim(); -+ const message = typeof failure?.message === "string" ? failure.message.trim() : ""; -+ return message || fallback; -+} -+function settleWorkflowRead(value, fallback = "Could not read workflows") { -+ if (!value || value.ok === false || !Array.isArray(value.workflows)) { -+ const raw = typeof value?.error === "string" && value.error.trim() ? value.error : ""; -+ const error = workflowCallMissing(value) || workflowCallMissing(raw) ? WORKFLOW_PLUGIN_HINT : raw || fallback; -+ return { phase: "error", error, pack: null }; -+ } -+ const templates = Array.isArray(value.templates) ? value.templates : []; -+ const workflows = value.workflows; -+ const pack = { ...value, templates, workflows }; -+ if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack }; -+ return { phase: "ready", error: "", pack }; -+} -+function readWorkflows(call4, { timeoutMs = READ_MS } = {}) { -+ return new Promise((resolve) => { -+ let settled = false; -+ const finish = (value) => { -+ if (settled) return; -+ settled = true; -+ clearTimeout(timer); -+ resolve(value); -+ }; -+ const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs); -+ Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null })); -+ }); +@@ -6830,8 +6830,8 @@ var FOLDER_CSS = ` + `; + + // src/client/schedules.js +-var import_react15 = require("react"); +-var POLL_MS = 3e4; ++var import_react25 = require("react"); ++var POLL_MS4 = 3e4; + var shared = null; + var listeners = /* @__PURE__ */ new Set(); + function publish(view) { +@@ -6839,8 +6839,8 @@ function publish(view) { + for (const listener of listeners) listener(view); + } + function useSchedules(actions) { +- const [view, setView] = (0, import_react15.useState)(shared); +- (0, import_react15.useEffect)(() => { ++ const [view, setView] = (0, import_react25.useState)(shared); ++ (0, import_react25.useEffect)(() => { + listeners.add(setView); + let stopped = false; + let timer; +@@ -6848,7 +6848,7 @@ function useSchedules(actions) { + if (!stopped) publish(next); + }).catch(() => { + }).finally(() => { +- if (!stopped) timer = setTimeout(load, POLL_MS); ++ if (!stopped) timer = setTimeout(load, POLL_MS4); + }); + load(); + return () => { +@@ -6873,7 +6873,8 @@ function weekdayNames(days) { + function everyText(seconds) { + if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); + if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); +- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); ++ if (seconds % 60 === 0) return t("Every {count} minutes", { count: seconds / 60 }); ++ return t("Every {count} seconds", { count: seconds }); + } + function timingText(task) { + const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; +@@ -6887,8 +6887,8 @@ function timingText(task) { + return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; + case "weekly": { + const days = (task.weekdays ?? []).join(); +- 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}`; ++ const text2 = 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 `${text2}${zone}`; + } + case "cron": + return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; +@@ -6896,11 +6896,25 @@ function timingText(task) { + return ""; + } } --function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { -- const [adding, setAdding] = (0, import_react23.useState)(false); -- const [confirming, setConfirming] = (0, import_react23.useState)(false); -- (0, import_react23.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 4e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const outside = roster.bots.filter((bot) => !room2.members.includes(bot.id)); -- const admin = roster.byId[room2.admin]; -- const mode = shownMode(room2, roster); -- const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; -- const update = (patch) => run(actions.updateRoom(room2.id, patch)); -- const openBot = (id) => { -- actions.closeSettings(); -- actions.openSession(id); -+function moveNode(nodes, id, x, y) { -+ return nodes.map((node) => node.id === id ? { ...node, x, y } : node); -+} -+function connectNodes(edges, from, to) { -+ if (!from || !to || from === to) return edges; -+ if (edges.some((edge) => edge.from === from && edge.to === to)) return edges; -+ return edges.concat([{ from, to }]); -+} -+function dropBot(nodes, bot, x, y) { -+ const ids = new Set(nodes.map((node) => node.id)); -+ let n = nodes.length + 1; -+ while (ids.has(`n${n}`)) n += 1; -+ return nodes.concat([{ id: `n${n}`, name: bot.name, botId: bot.id, role: bot.role ?? "", x, y }]); +-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)(() => { ++var nextText = (task) => task.status === "paused" ? t("Paused") : task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); ++function lastRunText(run) { ++ if (run === void 0 || !Number.isFinite(run.at)) return ""; ++ const time = dayAndTime(run.at); ++ switch (run.result) { ++ case "running": ++ return t("Running since {time}", { time }); ++ case "stopped": ++ return t("Last run {time} · Stopped", { time }); ++ case "failed": ++ return t("Last run {time} · Failed: {reason}", { time, reason: hostText(run.reason || "Unknown error") }); ++ default: ++ return t("Last run {time}", { time }); ++ } +} -+function removeNode(nodes, edges, id) { -+ return { -+ nodes: nodes.filter((node) => node.id !== id), -+ edges: edges.filter((edge) => edge.from !== id && edge.to !== id) ++function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) { ++ const [confirming, setConfirming] = (0, import_react25.useState)(false); ++ const [busy, setBusy] = (0, import_react25.useState)(false); ++ (0, import_react25.useEffect)(() => { + if (!confirming) return void 0; + const timer = setTimeout(() => setConfirming(false), 3e3); + return () => clearTimeout(timer); +@@ -6913,20 +6913,49 @@ function TaskRow({ task, actions, onView, showBot, roster }) { + setBusy(true); + void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; -- const remove = () => { -- if (!confirming) { -- setConfirming(true); -- return; -+} -+function chainOf(item) { -+ if (typeof item?.chain === "string" && item.chain) return item.chain; -+ if (!Array.isArray(item?.layers) || !Array.isArray(item?.nodes)) return ""; -+ const name = (id) => item.nodes.find((node) => node.id === id)?.name ?? id; -+ return item.layers.map((layer) => layer.map(name).join(" ‖ ")).join(" → "); -+} -+function openedGraph(pack, opened) { -+ if (!pack || !opened) return null; -+ if (opened.source === "own") return (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; -+ return (pack.templates ?? []).find((item) => item.id === opened.id) ?? (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; -+} -+function statusLabel2(status) { -+ if (status === "running") return t("Running"); -+ if (status === "done") return t("Done"); -+ if (status === "failed") return t("Failed"); -+ if (status === "idle") return t("Idle"); -+ if (status === "skipped") return t("Skipped"); -+ return t("Queued"); -+} -+function hitNode(event) { -+ const direct = event.target?.dataset?.node ? event.target : null; -+ const nested = event.target?.closest?.("[data-node]"); -+ return (nested ?? direct)?.dataset?.node || ""; -+} -+var WIRE_SPAN = 480; -+var NODE_WIRE_OUT = 56; -+var CARD_HALF = 84; -+var CARD_H = 72; -+var FRAME_SIDE = 108; -+var FRAME_LABEL = 18; -+var FRAME_FOOT = 10; -+var MIN_FIT = 0.5; -+function wirePath(from, to) { -+ const x1 = from.x + WIRE_SPAN; -+ const y1 = from.y + NODE_WIRE_OUT; -+ const x2 = to.x + WIRE_SPAN; -+ const y2 = to.y; -+ const mid = (y1 + y2) / 2; -+ return `M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`; -+} -+function fitScale(nodes, width) { -+ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -+ if (!(width > 0) || reach === 0) return 1; -+ return Math.min(1, Math.max(MIN_FIT, width / 2 / reach)); -+} -+function fitLayout(nodes, width) { -+ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -+ const scale = fitScale(nodes, width); -+ const spare = width > 0 && reach > 0 ? reach * scale - width / 2 : 0; -+ const pad2 = spare > 1e-6 ? Math.ceil(spare) : 0; -+ const centre = width > 0 ? pad2 + width / 2 : 0; -+ return { -+ reach, -+ scale, -+ pad: pad2, -+ left: centre - reach * scale, -+ right: centre + reach * scale, -+ scrollWidth: Math.max(width + pad2, centre + reach * scale) ++ const toggle = () => { ++ setBusy(true); ++ const step = task.status === "paused" ? actions.scheduleResume : actions.schedulePause; ++ void step(task.sessionId, task.id).then((next) => { ++ setBusy(false); ++ onView(next); ++ }, () => setBusy(false)); + }; -+} -+function measureCanvasWidth(el) { -+ const width = el?.clientWidth; -+ return Number.isFinite(width) && width > 0 ? width : 0; -+} -+function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) { -+ const gesture = (0, import_react36.useRef)(null); -+ const canvasRef = (0, import_react36.useRef)(null); -+ const [measuredWidth, setCanvasWidth] = (0, import_react36.useState)(0); -+ const nodes = graph.nodes ?? []; -+ const edges = graph.edges ?? []; -+ const status = graph.run?.nodeStatus ?? null; -+ const locals = (roster?.bots ?? []).filter((bot) => bot && bot.remote !== true && !nodes.some((node) => node.botId === bot.id)); -+ const height = Math.max(420, ...nodes.map((node) => (node.y ?? 0) + 150), 0); -+ const canvasWidth = measuredWidth || knownWidth; -+ const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth); -+ const drawnHeight = Math.round(height * scale); -+ (0, import_react36.useEffect)(() => { -+ const el = canvasRef.current; -+ const measure = () => { -+ const width = measureCanvasWidth(el); -+ if (width > 0 && Math.abs(width - canvasWidth) > 0.5) setCanvasWidth(width); -+ }; -+ measure(); -+ if (typeof ResizeObserver !== "function" || !el) return void 0; -+ const observer = new ResizeObserver(measure); -+ observer.observe(el); -+ return () => observer.disconnect(); -+ }, [canvasWidth]); -+ const end = (event) => { -+ const current = gesture.current; -+ gesture.current = null; -+ if (!current || !editing) return; -+ if (current.kind === "move") { -+ const x = Math.round(current.x + (event.clientX - current.ox) / scale); -+ const y = Math.round(current.y + (event.clientY - current.oy) / scale); -+ onCommit?.({ nodes: moveNode(nodes, current.id, x, y), edges }); -+ } else if (current.kind === "wire") { -+ const next = connectNodes(edges, current.from, hitNode(event)); -+ if (next !== edges) onCommit?.({ nodes, edges: next }); -+ } else if (current.kind === "add" && event.target?.dataset?.plane) { -+ const x = Number.isFinite(event.offsetX) ? Math.round(event.offsetX / scale) : 24; -+ const y = Number.isFinite(event.offsetY) ? Math.round(event.offsetY / scale) : 24; -+ onCommit?.({ nodes: dropBot(nodes, current.bot, x, y), edges }); - } -- void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { -- if (done) onDeleted?.(); -- }); - }; -- return (0, import_react23.createElement)( -+ return (0, import_react36.createElement)( ++ const [retryError, setRetryError] = (0, import_react25.useState)(""); ++ const retry = () => { ++ setBusy(true); ++ setRetryError(""); ++ void actions.scheduleRetry(task.sessionId, task.id).then((next) => { ++ setBusy(false); ++ onView(next); ++ }, (error) => { ++ setBusy(false); ++ setRetryError(hostText(error?.message ?? String(error))); ++ }); ++ }; + const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; +- return (0, import_react15.createElement)( ++ const stoppable = task.status === "active" || task.status === "paused"; ++ const failed = task.lastRun?.result === "failed"; ++ const last = lastRunText(task.lastRun); ++ return (0, import_react25.createElement)( "div", -- { className: "bt-bs" }, -- (0, import_react23.createElement)( -+ { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end }, -+ editing ? (0, import_react36.createElement)( - "div", -- { className: "bt-bs-hero" }, -- (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-bs-who" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), -- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Profile") }, -- (0, import_react23.createElement)( -- Row, -- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) -- ), -- (0, import_react23.createElement)( -- Row, -- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) -- ) -- ), -- (0, import_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.")}`) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react23.createElement)( -+ { className: "bt-wf-palette" }, -+ (0, import_react36.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), -+ locals.map((bot) => (0, import_react36.createElement)("button", { -+ key: bot.id, -+ type: "button", -+ "data-bot": bot.id, -+ onPointerDown: (event) => { -+ event.preventDefault?.(); -+ gesture.current = { kind: "add", bot }; -+ } -+ }, bot.name)) -+ ) : null, -+ // A node's system prompt is the run Bot's prompt (plugins/workflow -+ // nodeConfig). The sidebar panel has this field, so the Settings editor -+ // writes the same graph instead of only drawing it (#1393). -+ editing ? (0, import_react36.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react36.createElement)( -+ "label", -+ { key: node.id, className: "bt-wf-prompt" }, -+ (0, import_react36.createElement)("span", null, `${node.name} · ${t("System prompt")}`), -+ (0, import_react36.createElement)("textarea", { -+ "data-prompt": node.id, -+ rows: 3, -+ defaultValue: node.prompt || "", -+ onPointerDown: (event) => event.stopPropagation?.(), -+ onBlur: (event) => { -+ const prompt = event.target?.value ?? ""; -+ if (prompt === (node.prompt || "")) return; -+ onCommit?.({ nodes: nodes.map((item) => item.id === node.id ? { ...item, prompt } : item), edges }); -+ } -+ }) -+ ))) : null, -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, -+ (0, import_react36.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)( -+ { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, -+ (0, import_react36.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 -+ { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, -+ (0, import_react36.createElement)( -+ "svg", -+ { -+ className: "bt-wf-wires", -+ "aria-hidden": true, -+ width: WIRE_SPAN * 2, -+ height, -+ viewBox: `0 0 ${WIRE_SPAN * 2} ${height}` -+ }, -+ (0, import_react36.createElement)( -+ "defs", -+ null, -+ (0, import_react36.createElement)("marker", { -+ id: "bt-wf-arrow", -+ viewBox: "0 0 10 10", -+ refX: "8", -+ refY: "5", -+ markerWidth: "6", -+ markerHeight: "6", -+ orient: "auto" -+ }, (0, import_react36.createElement)("path", { d: "M0 0 10 5 0 10z" })) -+ ), -+ edges.map((edge) => { -+ const from = nodes.find((node) => node.id === edge.from); -+ const to = nodes.find((node) => node.id === edge.to); -+ if (!from || !to) return null; -+ return (0, import_react36.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" }); -+ }) - ), -- 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 })) }) -+ (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => { -+ const members = layer.map((id) => nodes.find((node) => node.id === id)).filter(Boolean); -+ if (members.length < 2) return null; -+ const left = Math.min(...members.map((node) => node.x ?? 0)) - FRAME_SIDE; -+ const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE; -+ const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL; -+ const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT; -+ return (0, import_react36.createElement)("div", { -+ key: layer.join("+"), -+ className: "bt-wf-par", -+ style: { left, top, width: right - left, height: bottom - top } -+ }, (0, import_react36.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join"))); -+ }), -+ nodes.map((node) => { -+ const state = status ? status[node.id] ?? "queued" : "idle"; -+ return (0, import_react36.createElement)( -+ "div", -+ { -+ key: node.id, -+ "data-node": node.id, -+ className: `bt-wf-node ${state}${editing ? " editing" : ""}`, -+ style: { left: (node.x ?? 0) - CARD_HALF, top: node.y ?? 0 }, -+ onPointerDown: (event) => { -+ if (!editing || event.target?.dataset?.port || event.target?.dataset?.remove) return; -+ gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 }; -+ } -+ }, -+ editing ? (0, import_react36.createElement)("button", { -+ type: "button", -+ className: "bt-wf-port", -+ "data-port": "out", -+ "aria-label": node.name, -+ onPointerDown: (event) => { -+ event.preventDefault?.(); -+ event.stopPropagation?.(); -+ gesture.current = { kind: "wire", from: node.id }; -+ } -+ }) : null, -+ editing ? (0, import_react36.createElement)("button", { -+ type: "button", -+ className: "bt-wf-remove", -+ "data-remove": node.id, -+ "aria-label": t("Remove"), -+ onClick: (event) => { -+ event.stopPropagation?.(); -+ const next = removeNode(nodes, edges, node.id); -+ onCommit?.(next); -+ } -+ }, "×") : null, -+ (0, import_react36.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)), -+ (0, import_react36.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)), -+ (0, import_react36.createElement)( -+ "span", -+ { className: "bt-wf-copy" }, -+ (0, import_react36.createElement)("span", { className: "bt-wf-name" }, node.name), -+ (0, import_react36.createElement)("span", { className: "bt-wf-role" }, node.role ?? "") -+ ) -+ ); -+ }) -+ ) -+ ) -+ ) -+ ); +- { 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-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, ++ showBot ? (0, import_react25.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, ++ (0, import_react25.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 ++ (0, import_react25.createElement)("span", { className: "bt-task-title" }, task.title), ++ (0, import_react25.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_react25.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, ++ last ? (0, import_react25.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, ++ retryError ? (0, import_react25.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null + ), +- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), +- (0, import_react15.createElement)("button", { ++ (0, import_react25.createElement)("span", { className: "bt-task-next" }, nextText(task)), ++ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, ++ onEdit ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react25.createElement)(PencilIcon)) : null, ++ onCopy ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react25.createElement)(DuplicateIcon)) : null, ++ stoppable ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, ++ (0, import_react25.createElement)("button", { + type: "button", + className: "bt-mini bt-task-remove", + "data-confirm": confirming || void 0, +@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { + "aria-label": confirming ? t("Click again to delete") : t("Delete task"), + title: confirming ? void 0 : t("Delete task"), + onClick: remove +- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) ++ }, confirming ? t("Delete") : (0, import_react25.createElement)(TrashIcon)) + ); + } + var pad = (number) => String(number).padStart(2, "0"); +@@ -6944,27 +6944,49 @@ function inAnHour() { + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; + } + var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; +-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)(""); ++var UNITS = [["seconds", 1, "seconds"], ["minutes", 60, "minutes"], ["hours", 3600, "hours"], ["days", 86400, "days"]]; ++function localInput(ms) { ++ const date = new Date(ms); ++ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; +} -+function Row2({ item, source, onOpen, onRename, onDelete }) { -+ return (0, import_react36.createElement)( -+ "button", -+ { -+ type: "button", -+ className: "bt-set-row", -+ "data-workflow": item.id, -+ "data-source": source, -+ onClick: () => onOpen?.(item) -+ }, -+ (0, import_react36.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react36.createElement)("span", null, item.name), -+ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") ++function formStart(task) { ++ const seconds = task?.kind === "every" ? Number(task.everySeconds) : 3600; ++ const [unit, size] = seconds % 86400 === 0 ? ["days", 86400] : seconds % 3600 === 0 ? ["hours", 3600] : seconds % 60 === 0 ? ["minutes", 60] : ["seconds", 1]; ++ const once = task?.kind === "at" || task?.kind === "after"; ++ return { ++ title: task?.title ?? "", ++ prompt: task?.prompt ?? "", ++ kind: once ? "at" : task?.kind ?? "daily", ++ at: once && !Number.isNaN(Date.parse(task.scheduledAt)) ? localInput(Date.parse(task.scheduledAt)) : inAnHour(), ++ time: task?.time ? clock2(task.time) : "09:00", ++ weekdays: task?.kind === "weekly" && Array.isArray(task.weekdays) ? task.weekdays : [1, 2, 3, 4, 5], ++ count: Math.max(1, Math.round(seconds / size)), ++ unit, ++ timeZone: task?.timeZone || localZone() ++ }; ++} ++function TaskForm({ roster, actions, start, task, onDone, onCancel }) { ++ const first = formStart(task); ++ const [target, setTarget] = (0, import_react25.useState)(task ? task.botId ?? task.roomId : start ?? ""); ++ const [title, setTitle] = (0, import_react25.useState)(first.title); ++ const [prompt, setPrompt] = (0, import_react25.useState)(first.prompt); ++ const [kind, setKind] = (0, import_react25.useState)(first.kind); ++ const [at, setAt] = (0, import_react25.useState)(first.at); ++ const [time, setTime] = (0, import_react25.useState)(first.time); ++ const [weekdays, setWeekdays] = (0, import_react25.useState)(first.weekdays); ++ const [count, setCount] = (0, import_react25.useState)(first.count); ++ const [unit, setUnit] = (0, import_react25.useState)(first.unit); ++ const [busy, setBusy] = (0, import_react25.useState)(false); ++ const [error, setError] = (0, import_react25.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 } : {} }; ++ if (kind === "cron") return { kind: "keep" }; ++ if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone }; ++ if (kind === "every") return { kind, seconds: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; ++ return { kind, time, timeZone: first.timeZone, ...kind === "weekly" ? { weekdays } : {} }; + }; + const submit = (event) => { + event.preventDefault(); +- if (busy) return; ++ if (busy || target === "") return; + setBusy(true); + setError(""); + let when3; +@@ -6975,151 +6975,298 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { + setBusy(false); + return; + } +- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { ++ const saved = task ? actions.scheduleUpdate({ sessionId: task.sessionId, id: task.id, target, title, prompt, when: when3 }) : actions.scheduleCreate({ target, title, prompt, when: when3 }); ++ void saved.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)( ++ return (0, import_react25.createElement)( + "form", + { className: "bt-set-card bt-task-form", onSubmit: submit }, +- (0, import_react15.createElement)( ++ (0, import_react25.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)( ++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), ++ (0, import_react25.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_react25.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), ++ (0, import_react25.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react25.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), ++ roster.rooms.length > 0 ? (0, import_react25.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react25.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null + ) ), -- (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)) -+ source === "own" ? (0, import_react36.createElement)( -+ "span", -+ { className: "bt-set-actions" }, -+ (0, import_react36.createElement)("button", { -+ type: "button", -+ className: "bt-set-action", -+ "aria-label": t("Rename"), -+ title: t("Rename"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onRename?.(item); -+ } -+ }, "✎"), -+ (0, import_react36.createElement)("button", { -+ type: "button", -+ className: "bt-set-action", -+ "aria-label": t("Delete"), -+ title: t("Delete"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onDelete?.(item); -+ } -+ }, "×") -+ ) : null, -+ (0, import_react36.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›") -+ ); +- (0, import_react15.createElement)( ++ (0, import_react25.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Name"))), ++ (0, import_react25.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)( ++ (0, import_react25.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), ++ (0, import_react25.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) + ), +- (0, import_react15.createElement)( ++ (0, import_react25.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)( ++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("When"))), ++ (0, import_react25.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react25.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), ++ (0, import_react25.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", { ++ kind === "cron" ? (0, import_react25.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, ++ kind === "at" ? (0, import_react25.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_react25.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react25.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, ++ kind === "daily" || kind === "weekly" ? (0, import_react25.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_react25.createElement)( ++ import_react25.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)( ++ (0, import_react25.createElement)("span", { className: "bt-task-every" }, t("Every")), ++ (0, import_react25.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_react25.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))) ++ UNITS.map(([id, , label]) => (0, import_react25.createElement)("option", { key: id, value: id }, t(label))) + ) + ) : 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)( ++ error ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, ++ (0, import_react25.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")) ++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), ++ (0, import_react25.createElement)( ++ "button", ++ { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, ++ task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") ++ ) + ) + ); + } +-function AutomationOff() { +- return (0, import_react15.createElement)( ++function openPlugins(actions) { ++ actions.closeSettings(); ++ actions.selectPanel("plugins"); +} -+function alertCopy(error) { -+ if (error === WORKFLOW_PLUGIN_HINT) return t("The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."); -+ return error || t("Could not read workflows"); ++function AutomationOff({ actions }) { ++ return (0, import_react25.createElement)( + "div", + { className: "bt-set-card bt-task-off" }, +- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), +- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) ++ (0, import_react25.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react25.createElement)(ClockIcon)), ++ (0, import_react25.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), ++ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), ++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ++ ); +} -+function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, canvasWidth = 0, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) { -+ const graph = phase === "ready" ? openedGraph(pack, opened) : null; -+ const templates = pack?.templates ?? []; -+ const templateIds = new Set(templates.map((item) => item.id)); -+ const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); -+ const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; -+ return (0, import_react36.createElement)( -+ "div", -+ { className: "bt-wf-page" }, -+ (0, import_react36.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_react36.createElement)( -+ "div", -+ { className: "bt-error", role: "alert" }, -+ alertCopy(error), -+ phase === "error" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null -+ ) : null, -+ phase === "loading" ? (0, import_react36.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null, -+ phase === "empty" ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null, -+ phase === "ready" && !graph ? (0, import_react36.createElement)( -+ "div", -+ { className: "bt-wf-lists" }, -+ (0, import_react36.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react36.createElement)("h3", null, t("Factory presets")), -+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")), -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ templates.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => { -+ const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id); -+ const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item; -+ return (0, import_react36.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); -+ }) -+ ) -+ ), -+ (0, import_react36.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react36.createElement)("h3", null, t("Yours")), -+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) -+ ) - ) -- ) -+ ) : null, -+ graph ? (0, import_react36.createElement)( ++function QuietHoursCard({ view, actions }) { ++ const quiet = view.quietHours ?? { on: false }; ++ const [busy, setBusy] = (0, import_react25.useState)(false); ++ const [error, setError] = (0, import_react25.useState)(""); ++ const [from, setFrom] = (0, import_react25.useState)(quiet.from ?? "22:00"); ++ const [to, setTo] = (0, import_react25.useState)(quiet.to ?? "07:00"); ++ const unchecked = view.quietUnchecked === true; ++ const refresh = () => { ++ void Promise.resolve(actions.schedules?.()).then((next) => { ++ if (next) publish(next); ++ }).catch(() => { ++ }); ++ }; ++ const save = (next) => { ++ if (busy) return; ++ setBusy(true); ++ setError(""); ++ const shown = next.on ? { on: true, from: next.from ?? from, to: next.to ?? to } : { on: false }; ++ void Promise.resolve(actions.setPrefs({ quietHours: next })).then(() => { ++ publish({ ...view, quietHours: shown }); ++ refresh(); ++ }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); ++ }; ++ return (0, import_react25.createElement)( ++ "section", ++ { className: "bt-set-section" }, ++ (0, import_react25.createElement)("h3", null, t("Quiet hours")), ++ (0, import_react25.createElement)( + "div", -+ { className: "bt-wf-editor" }, -+ (0, import_react36.createElement)( ++ { className: "bt-set-card" }, ++ (0, import_react25.createElement)( + "div", -+ { className: "bt-wf-actions" }, -+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), -+ showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, -+ opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null ++ { className: "bt-set-row bt-bs-row" }, ++ (0, import_react25.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react25.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), ++ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) ++ ), ++ (0, import_react25.createElement)("button", { ++ type: "button", ++ className: "bt-soft bt-soft-sm", ++ disabled: busy, ++ "aria-pressed": quiet.on, ++ onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) ++ }, quiet.on ? t("Turn off") : t("Turn on")) + ), -+ (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name), -+ graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, -+ (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, { -+ graph, -+ roster, -+ onCommit, -+ width: canvasWidth, -+ editing: opened?.source === "own" && editing === true -+ }) -+ ) : null ++ quiet.on ? (0, import_react25.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row" }, ++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Window"))), ++ (0, import_react25.createElement)( ++ "div", ++ { className: "bt-task-when" }, ++ (0, import_react25.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), ++ (0, import_react25.createElement)("span", { className: "bt-task-every" }, "–"), ++ (0, import_react25.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), ++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) ++ ) ++ ) : null, ++ error ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, ++ quiet.on && unchecked ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : 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_react36.useState)(workflow?.name ?? ""); -+ return (0, import_react36.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( -+ { className: "bt-modal-mask", onClick: (event) => { -+ if (event.target === event.currentTarget) onClose?.(); -+ } }, -+ (0, import_react36.createElement)( + 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 [adding, setAdding] = (0, import_react25.useState)(Boolean(start)); ++ const [editing, setEditing] = (0, import_react25.useState)(null); ++ if (view === null) return (0, import_react25.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); ++ if (!view.available) return (0, import_react25.createElement)(AutomationOff, { actions }); + const shown = (next) => { + publish(next); + setAdding(false); ++ setEditing(null); ++ }; ++ const edit = (task) => { ++ setAdding(false); ++ setEditing(task); + }; + 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)]) ++ ...roster.bots.map((bot) => [bot.id, (0, import_react25.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_react25.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)( ++ (0, import_react25.createElement)(QuietHoursCard, { key: "quiet", view, actions }), ++ adding ? (0, import_react25.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react25.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { ++ setEditing(null); ++ setAdding(true); ++ } }, (0, import_react25.createElement)(PlusIcon), t("New task"))), ++ byChat.length === 0 && !adding ? (0, import_react25.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react25.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 }))) ++ (0, import_react25.createElement)("h3", { className: "bt-task-head" }, avatar, name), ++ (0, import_react25.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) + )) + ]; + } ++function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { ++ const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; ++ const [copying, setCopying] = (0, import_react25.useState)(null); ++ const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0; ++ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react25.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 })); ++ return (0, import_react25.createElement)( ++ import_react25.Fragment, ++ null, ++ open ? (0, import_react25.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, ++ source ? (0, import_react25.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => { ++ setCopying(null); ++ onDone(next); ++ }, onCancel: () => setCopying(null) }) : null, ++ rows.length + extra.length > 0 ? (0, import_react25.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null ++ ); ++} ++function CopyRow({ task, roster, actions, onView, onCancel }) { ++ const others = [ ++ ...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]), ++ ...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name]) ++ ]; ++ const [target, setTarget] = (0, import_react25.useState)(""); ++ const [busy, setBusy] = (0, import_react25.useState)(false); ++ const [error, setError] = (0, import_react25.useState)(""); ++ const submit = (event) => { ++ event.preventDefault(); ++ if (busy || target === "") return; ++ setBusy(true); ++ setError(""); ++ void actions.scheduleCreate({ target, copyFrom: { sessionId: task.sessionId, id: task.id } }).then(onView, (failure) => { ++ setBusy(false); ++ setError(hostText(failure?.message ?? String(failure))); ++ }); ++ }; ++ return (0, import_react25.createElement)( ++ "form", ++ { className: "bt-set-row bt-task-copy", onSubmit: submit }, ++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))), ++ (0, import_react25.createElement)( ++ "select", ++ { className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) }, ++ (0, import_react25.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), ++ others.map(([id, name]) => (0, import_react25.createElement)("option", { key: id, value: id }, name)) ++ ), ++ (0, import_react25.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")), ++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), ++ error ? (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null ++ ); ++} + function BotSchedules({ bot, roster, actions }) { + const view = useSchedules(actions); ++ const [editing, setEditing] = (0, import_react25.useState)(null); + if (view === null) return null; + const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; +- return (0, import_react15.createElement)( ++ return (0, import_react25.createElement)( + "section", + { className: "bt-set-section" }, +- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), +- (0, import_react15.createElement)( ++ (0, import_react25.createElement)("h3", null, t("Scheduled tasks")), ++ !view.available ? (0, import_react25.createElement)( "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( -+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, -+ (0, import_react36.createElement)("h3", null, t("Rename workflow")), -+ (0, import_react36.createElement)( -+ "label", -+ { className: "bt-field" }, -+ t("Name"), -+ (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) -+ ), -+ (0, import_react36.createElement)( + { className: "bt-set-card" }, +- !view.available ? (0, import_react15.createElement)( ++ (0, import_react25.createElement)( "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) -+ { className: "bt-modal-actions" }, -+ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) + { 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")) +- ] +- ) ++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), ++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ++ ) ++ ) : (0, import_react25.createElement)(TaskList, { ++ tasks, ++ editing, ++ roster, ++ actions, ++ onEdit: setEditing, ++ onDone: (next) => { ++ publish(next); ++ setEditing(null); ++ }, ++ onCancel: () => setEditing(null), ++ extra: [(0, import_react25.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react25.createElement)(PlusIcon), t("New task"))] ++ }) + ); + } + 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)( ++ return (0, import_react25.createElement)( + "div", + null, +- (0, import_react15.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), +- tasks.length === 0 ? (0, import_react15.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 in Harness settings.") : t("No scheduled tasks yet. Add one")) : (0, import_react15.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react15.createElement)( ++ (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), ++ tasks.length === 0 ? (0, import_react25.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.") : t("No scheduled tasks yet. Add one")) : (0, import_react25.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react25.createElement)( + "button", + { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, +- (0, import_react15.createElement)(ClockIcon), +- (0, import_react15.createElement)("span", { className: "bt-task-line-title" }, task.title), +- (0, import_react15.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) ++ (0, import_react25.createElement)(ClockIcon), ++ (0, import_react25.createElement)("span", { className: "bt-task-line-title" }, task.title), ++ (0, import_react25.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) + ))) + ); + } + var SCHEDULE_CSS = ` + .bt-task{gap:10px} ++.bt-task-copy{flex-wrap:wrap;gap:8px} ++.bt-task-copy .bt-task-bot{flex:1;min-width:140px;max-width:100%} + .bt-task[data-done]{opacity:.6} + .bt-task[data-busy]{opacity:.4} + .bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} +@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = ` + .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} + .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} + .bt-task-remove svg{width:14px;height:14px} ++.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} ++.bt-task-tool svg{width:14px;height:14px} ++.bt-task-pause{height:26px;color:var(--bt-ink-2)} ++.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere} ++.bt-task-last[data-failed]{color:#c21d2e} ++.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)} + .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} + .bt-task-head{display:flex;align-items:center;gap:6px} + .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} +@@ -7158,12 +7158,12 @@ var SCHEDULE_CSS = ` + `; + + // src/client/capability-settings.js +-var import_react16 = require("react"); ++var import_react26 = require("react"); + var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); +-var parseMap = (text, label) => { ++var parseMap = (text2, label) => { + let value; + try { +- value = JSON.parse(text || "{}"); ++ value = JSON.parse(text2 || "{}"); + } catch { + throw new Error(t("{label} must be valid JSON.", { label })); + } +@@ -7172,11 +7172,11 @@ var parseMap = (text, label) => { + }; + var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); + function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { +- return (0, import_react16.createElement)( ++ return (0, import_react26.createElement)( + "label", + { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t(label))), +- (0, import_react16.createElement)(multiline ? "textarea" : "input", { ++ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t(label))), ++ (0, import_react26.createElement)(multiline ? "textarea" : "input", { + className: multiline ? "bt-input bt-input-area" : "bt-input", + value, + disabled, +@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl + ); + } + function CapabilityForm({ draft, setDraft, items, actions, onClose }) { +- const [error, setError] = (0, import_react16.useState)(""); ++ const [error, setError] = (0, import_react26.useState)(""); + const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); + const submit = async (event) => { + event.preventDefault(); +@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { + if (draft.kind === "mcp" && draft.transport === "stdio") { + payload.transport = "stdio"; + payload.command = draft.command; +- payload.args = draft.argsText.split("\n").filter((line) => line !== ""); ++ payload.args = draft.argsText.split("\n").filter((line2) => line2 !== ""); + payload.env = parseMap(draft.envText, t("Environment")); + } else if (draft.kind === "mcp") { + payload.transport = "streamable-http"; +@@ -7219,95 +7219,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { + } + }; + const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); +- return (0, import_react16.createElement)( ++ return (0, import_react26.createElement)( + "form", + { className: "bt-connector-form", onSubmit: submit }, +- (0, import_react16.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), ++ (0, import_react26.createElement)( + "label", + { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Type"))), +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Type"))), ++ (0, import_react26.createElement)( + "select", + { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, +- ["mcp", "skill", "pack"].map((kind) => (0, import_react16.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) ++ ["mcp", "skill", "pack"].map((kind) => (0, import_react26.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) ) -- ), -- admin === null ? null : (0, import_react23.createElement)( -+ ) -+ ); -+} -+function DeleteModal({ workflow, onClose, onConfirm }) { -+ return (0, import_react36.createElement)( -+ "div", -+ { className: "bt-modal-mask", onClick: (event) => { -+ if (event.target === event.currentTarget) onClose?.(); -+ } }, -+ (0, import_react36.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( -+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, -+ (0, import_react36.createElement)("h3", null, t("Delete workflow")), -+ (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), -+ (0, import_react36.createElement)( - "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) -+ { className: "bt-modal-actions" }, -+ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) + ), +- (0, import_react16.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), +- draft.kind === "mcp" ? (0, import_react16.createElement)( ++ (0, import_react26.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), ++ draft.kind === "mcp" ? (0, import_react26.createElement)( + "label", + { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), ++ (0, import_react26.createElement)( + "select", + { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, +- (0, import_react16.createElement)("option", { value: "stdio" }, t("Local command")), +- (0, import_react16.createElement)("option", { value: "streamable-http" }, t("HTTP server")) ++ (0, import_react26.createElement)("option", { value: "stdio" }, t("Local command")), ++ (0, import_react26.createElement)("option", { value: "streamable-http" }, t("HTTP server")) ) -- ), -- 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")) + ) : null, +- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react16.createElement)( ++ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react26.createElement)( + "div", + null, +- (0, import_react16.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), +- (0, import_react16.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), +- (0, import_react16.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ++ (0, import_react26.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), ++ (0, import_react26.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), ++ (0, import_react26.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) + ) : null, +- draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react16.createElement)( ++ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react26.createElement)( + "div", + null, +- (0, import_react16.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), +- (0, import_react16.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ++ (0, import_react26.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), ++ (0, import_react26.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) + ) : null, +- draft.kind === "skill" ? (0, import_react16.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, +- draft.kind === "pack" ? (0, import_react16.createElement)( ++ draft.kind === "skill" ? (0, import_react26.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, ++ draft.kind === "pack" ? (0, import_react26.createElement)( + "div", + { className: "bt-set-card" }, +- available.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react16.createElement)( ++ available.length === 0 ? (0, import_react26.createElement)("div", { className: "bt-set-row" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react26.createElement)( + "label", + { key: item.name, className: "bt-set-row bt-bs-row" }, +- (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, item.name)), +- (0, import_react16.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) ++ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, item.name)), ++ (0, import_react26.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) + )) + ) : null, +- error ? (0, import_react16.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, +- (0, import_react16.createElement)( ++ error ? (0, import_react26.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, ++ (0, import_react26.createElement)( + "div", + { className: "bt-connector-form-row" }, +- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), +- (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), ++ (0, import_react26.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) ) ); } --var GROUP_SETTINGS_CSS = ` --.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} --.bt-ng-step{animation:bt-veil-in .22s ease-out} --.bt-ng-label{display:inline-flex;align-items:center;gap:6px} --.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} --.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} --.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-ng-chip:active{scale:.96} --.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} --.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} --.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} --.bt-gs-member{gap:8px} --.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} --.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} --.bt-gs-add{gap:8px;color:var(--bt-ink-2)} --.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} --.bt-bs-stack .bt-seg{align-self:stretch} --@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} --`; -+function WorkflowsPage({ call: call4, roster }) { -+ const [phase, setPhase] = (0, import_react36.useState)("loading"); -+ const [pack, setPack] = (0, import_react36.useState)(null); -+ const [error, setError] = (0, import_react36.useState)(""); -+ const [opened, setOpened] = (0, import_react36.useState)(null); -+ const [editing, setEditing] = (0, import_react36.useState)(false); -+ const [renaming, setRenaming] = (0, import_react36.useState)(null); -+ const [deleting, setDeleting] = (0, import_react36.useState)(null); -+ const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows")); -+ const apply2 = (settled) => { -+ setPhase(settled.phase); -+ setPack(settled.pack); -+ setError(settled.error); -+ }; -+ (0, import_react36.useEffect)(() => { -+ let gone = false; -+ readWorkflows(invoke).then((settled) => { -+ if (!gone) apply2(settled); -+ }); -+ return () => { -+ gone = true; -+ }; -+ }, []); -+ const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); -+ (0, import_react36.useEffect)(() => { -+ if (!live || editing) return void 0; -+ let gone = false; -+ const timer = setInterval(() => { -+ readWorkflows(invoke).then((settled) => { -+ if (!gone) apply2(settled); -+ }); -+ }, POLL_MS6); -+ return () => { -+ gone = true; -+ clearInterval(timer); -+ }; -+ }, [live, editing]); -+ const retry = () => { -+ setPhase("loading"); -+ setError(""); -+ setOpened(null); -+ readWorkflows(invoke).then(apply2); -+ }; -+ const open = (item, source) => { -+ if (source === "factory") { -+ const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); -+ setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); -+ setEditing(false); -+ return; -+ } -+ setOpened({ source: "own", id: item.id }); -+ setEditing(false); -+ }; -+ const useTemplate = (id) => { -+ const template = typeof id === "string" ? id : ""; -+ if (!template) return; -+ Promise.resolve(invoke("workflow/create", { template })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ const before = new Set((pack?.workflows ?? []).map((item) => item.id)); -+ const made = settled.pack.workflows.find((item) => !before.has(item.id)); -+ setPack(settled.pack); -+ setPhase(settled.phase); -+ setError(""); -+ if (made) { -+ setOpened({ source: "own", id: made.id }); -+ setEditing(true); -+ } -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not copy this workflow"))); -+ }); -+ }; -+ const rename = (item) => { -+ setRenaming(item); -+ }; -+ const confirmRename = (workflowId, name) => { -+ if (!name.trim()) return; -+ Promise.resolve(invoke("workflow/rename", { workflowId, name: name.trim() })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ setPack(settled.pack); -+ setPhase("ready"); -+ setError(""); -+ setRenaming(null); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not rename this workflow"))); -+ }); -+ }; -+ const deleteWorkflow = (item) => { -+ setDeleting(item); -+ }; -+ const confirmDelete = (workflowId) => { -+ Promise.resolve(invoke("workflow/delete", { workflowId })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ setPack(settled.pack); -+ setPhase(settled.phase); -+ setError(""); -+ setDeleting(null); -+ if (opened?.id === workflowId) setOpened(null); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not delete this workflow"))); -+ }); -+ }; -+ const commit = (graph) => { -+ if (!opened || opened.source !== "own") return; -+ Promise.resolve(invoke("workflow/graph", { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ setPack(settled.pack); -+ setPhase("ready"); -+ setError(""); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not save this workflow"))); -+ }); -+ }; -+ return (0, import_react36.createElement)( + function CapabilityRow({ item, onEdit, onDelete, onApprove, confirming }) { + const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); + const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; +- return (0, import_react16.createElement)( ++ return (0, import_react26.createElement)( + "div", + { className: "bt-connector" }, +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)( + "div", + { className: "bt-connector-head" }, +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)( + "span", + { className: "bt-connector-text" }, +- (0, import_react16.createElement)("span", { className: "bt-connector-name" }, item.name), +- (0, import_react16.createElement)("span", { className: "bt-connector-status" }, secondary) ++ (0, import_react26.createElement)("span", { className: "bt-connector-name" }, item.name), ++ (0, import_react26.createElement)("span", { className: "bt-connector-status" }, secondary) + ), +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)( + "span", + { className: "bt-connector-actions" }, +- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, +- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), +- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) ++ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) + ) + ) + ); + } + function CapabilitiesPage({ roster, actions, run }) { + const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name)); +- const [draft, setDraft] = (0, import_react16.useState)(null); +- const [confirming, setConfirming] = (0, import_react16.useState)(""); +- (0, import_react16.useEffect)(() => { ++ const [draft, setDraft] = (0, import_react26.useState)(null); ++ const [confirming, setConfirming] = (0, import_react26.useState)(""); ++ (0, import_react26.useEffect)(() => { + if (!confirming) return void 0; + const timer = setTimeout(() => setConfirming(""), 4e3); + return () => clearTimeout(timer); +@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) { + setConfirming(""); + void run(actions.deleteCapability(item.name)); + }; +- return (0, import_react16.createElement)( ++ return (0, import_react26.createElement)( + "section", + { className: "bt-set-section" }, +- (0, import_react16.createElement)("h3", null, t("Shared capabilities")), +- (0, import_react16.createElement)( ++ (0, import_react26.createElement)("h3", null, t("Shared capabilities")), ++ (0, import_react26.createElement)( + "div", + { className: "bt-actions" }, +- (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) + ), +- draft ? (0, import_react16.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, +- (0, import_react16.createElement)( ++ draft ? (0, import_react26.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, ++ (0, import_react26.createElement)( + "div", + { className: "bt-set-card bt-connectors-card" }, +- items.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react16.createElement)(CapabilityRow, { ++ items.length === 0 ? (0, import_react26.createElement)("div", { className: "bt-set-row" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react26.createElement)(CapabilityRow, { + key: item.name, + item, + confirming: confirming === item.name, +@@ -7359,10 +7359,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow + const attached = new Set(bot.capabilities ?? []); + const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; + if (items.length === 0) return null; +- return (0, import_react16.createElement)( ++ return (0, import_react26.createElement)( + Card2, + { title: t("Capabilities") }, +- items.map((item) => (0, import_react16.createElement)(SwitchRow2, { ++ items.map((item) => (0, import_react26.createElement)(SwitchRow2, { + key: item.name, + label: item.name, + hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, +@@ -7372,9 +7372,55 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow + ); + } + ++// src/client/connector-bots.js ++var import_react27 = require("react"); ++function ownBots(roster) { ++ const me = roster?.self?.memberId; ++ return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); ++} ++var connectedConnectors = (roster) => (roster?.connectors ?? []).filter((connector) => connector.state !== "off" && connector.kind !== "sender" && !(connector.kind === "channel" && connector.mode !== "app")); ++var isOn = (bot, id) => bot.connectors?.[id] !== false; ++function ownHint(bot, id) { ++ return isOn(bot, id) ? t("On. This Bot can use it on every turn.") : t("Off. This Bot will not use it."); ++} ++function usageText(roster, id) { ++ const bots = ownBots(roster); ++ const own = bots.filter((bot) => isOn(bot, id)).length; ++ return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); ++} ++function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { ++ return (0, import_react27.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); ++} ++function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { ++ const bots = ownBots(roster); ++ return (0, import_react27.createElement)( + "div", -+ null, -+ (0, import_react36.createElement)(WorkflowsView, { -+ phase, -+ pack, -+ error, -+ opened, -+ editing, -+ roster, -+ onOpen: open, -+ onBack: () => { -+ setOpened(null); -+ setEditing(false); -+ }, -+ onEditing: setEditing, -+ onRetry: retry, -+ onUse: useTemplate, -+ onCommit: commit, -+ onRename: rename, -+ onDelete: deleteWorkflow -+ }), -+ renaming ? (0, import_react36.createElement)(RenameModal, { -+ workflow: renaming, -+ onClose: () => setRenaming(null), -+ onConfirm: (name) => confirmRename(renaming.id, name) -+ }) : null, -+ deleting ? (0, import_react36.createElement)(DeleteModal, { -+ workflow: deleting, -+ onClose: () => setDeleting(null), -+ onConfirm: () => confirmDelete(deleting.id) -+ }) : null ++ { className: "bt-connector-bots" }, ++ bots.length === 0 ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react27.createElement)( ++ "div", ++ { key: bot.id, className: "bt-connector-bot" }, ++ (0, import_react27.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) ++ )), ++ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) + ); +} - - // src/client/usage-data.js - var HOUR = 0; -@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { - } - return result; - } --function trimmed(value) { -- const text = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); -- return text.includes(".") ? text.replace(/\.?0+$/, "") : text; -+function trimmed2(value) { -+ const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); -+ return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; - } - function compact(count) { - for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { -- if (count >= size * 0.9995) return `${trimmed(count / size)}${unit}`; -+ if (count >= size * 0.9995) return `${trimmed2(count / size)}${unit}`; - } - return String(Math.round(count)); ++function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { ++ const connected = connectedConnectors(roster); ++ if (connected.length === 0) return null; ++ return (0, import_react27.createElement)( ++ Card2, ++ { title: t("Connectors") }, ++ connected.map((connector) => (0, import_react27.createElement)( ++ "div", ++ { key: connector.id, className: "bt-connector-bot" }, ++ (0, import_react27.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) ++ )) ++ ); ++} ++ + // src/client/memory-panel.js +-var import_react17 = require("react"); +-var POLL_MS2 = 1500; ++var import_react28 = require("react"); ++var POLL_MS5 = 1500; + var MENU_EXIT_MS2 = 120; + var counts = /* @__PURE__ */ new Map(); + var countListeners = /* @__PURE__ */ new Set(); +@@ -7384,13 +7384,13 @@ function noteCount(botId, count) { + for (const listener of countListeners) listener(); } -@@ -9294,11 +9294,11 @@ function percent(part, whole) { + function useMemoryCount(botId, actions) { +- const [count, setCount] = (0, import_react17.useState)(counts.get(botId) ?? null); +- (0, import_react17.useEffect)(() => { ++ const [count, setCount] = (0, import_react28.useState)(counts.get(botId) ?? null); ++ (0, import_react28.useEffect)(() => { + const listener = () => setCount(counts.get(botId) ?? null); + countListeners.add(listener); + listener(); + let stop = false; +- actions.memoryView?.(botId).then((view) => { ++ void actions.memoryView?.(botId).then((view) => { + if (!stop) noteCount(botId, view.entries.length); + }).catch(() => { + }); +@@ -7404,16 +7404,16 @@ function useMemoryCount(botId, actions) { + function MemoryRow({ bot, actions }) { + const count = useMemoryCount(bot.id, actions); + const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + null, +- (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")), +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Memory")), ++ (0, import_react28.createElement)( + "button", + { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, +- (0, import_react17.createElement)(MemoryIcon), +- (0, import_react17.createElement)("span", { className: "bt-mem-row-copy" }, label), +- (0, import_react17.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react17.createElement)(ChevronRightIcon)) ++ (0, import_react28.createElement)(MemoryIcon), ++ (0, import_react28.createElement)("span", { className: "bt-mem-row-copy" }, label), ++ (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon)) + ) + ); } - - // src/client/usage-page.js --var import_react24 = require("react"); -+var import_react37 = require("react"); - var HEAT_WEEKS = 53; - var TOP_SERIES = 7; - var RING_R = 42; --var RING = 2 * Math.PI * RING_R; -+var RING2 = 2 * Math.PI * RING_R; - var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; - var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; - var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) { - return dates({ year: "numeric", month: "long" }).format(bucket.start); +@@ -7428,45 +7428,45 @@ function ago(at, now) { + return format.format(-Math.round(hours / 24), "day"); } - function useTween(value) { -- const [shown, setShown] = (0, import_react24.useState)(0); -- const from = (0, import_react24.useRef)(0); -- (0, import_react24.useEffect)(() => { -+ const [shown, setShown] = (0, import_react37.useState)(0); -+ const from = (0, import_react37.useRef)(0); -+ (0, import_react37.useEffect)(() => { - if (reducedMotion() || typeof requestAnimationFrame !== "function") { - from.current = value; - setShown(value); -@@ -9363,9 +9363,9 @@ function useTween(value) { - }, [value]); - return shown; + function Skeleton({ label }) { +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + { className: "bt-mem-skel", "aria-busy": true }, +- [0, 1].map((block) => (0, import_react17.createElement)( ++ [0, 1].map((block) => (0, import_react28.createElement)( + "div", + { key: block, className: "bt-mem-skel-block" }, +- (0, import_react17.createElement)("span", { className: "bt-mem-skel-head" }), +- (0, import_react17.createElement)("span", null), +- (0, import_react17.createElement)("span", null), +- (0, import_react17.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) ++ (0, import_react28.createElement)("span", { className: "bt-mem-skel-head" }), ++ (0, import_react28.createElement)("span", null), ++ (0, import_react28.createElement)("span", null), ++ (0, import_react28.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) + )), +- label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null ++ label ? (0, import_react28.createElement)("div", { className: "bt-mem-skel-label" }, label) : null + ); } --var glyphBox = (size, icon2) => (0, import_react24.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react24.createElement)(icon2)); -+var glyphBox = (size, icon3) => (0, import_react37.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react37.createElement)(icon3)); - function ModelGlyph({ name, paint, size }) { -- return (0, import_react24.createElement)("span", { -+ return (0, import_react37.createElement)("span", { - className: "bt-us-model", - "aria-hidden": true, - style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +9382,7 @@ function ownerFace(owner, roster) { - sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, - paint: paintCss(colorOf(bot)), - stops: GRADIENTS[colorOf(bot)], -- glyph: (size) => (0, import_react24.createElement)(BotAvatar, { bot, size, badge: false, live: false }) -+ glyph: (size) => (0, import_react37.createElement)(BotAvatar, { bot, size, badge: false, live: false }) - }; - } - if (owner.kind === "room") { -@@ -9392,7 +9392,7 @@ function ownerFace(owner, roster) { - name: owner.name || t("Deleted group chat"), - sub: owner.deleted ? t("Deleted") : t("Group chat"), - paint: "var(--bt-us-rest)", -- glyph: (size) => room2 ? (0, import_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) -+ glyph: (size) => room2 ? (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) - }; + function Summary({ bot, view, regenerate }) { + if (view.entries.length === 0) { +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + { className: "bt-mem-empty" }, +- (0, import_react17.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react17.createElement)(MemoryIcon)), +- (0, import_react17.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), +- (0, import_react17.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) ++ (0, import_react28.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react28.createElement)(MemoryIcon)), ++ (0, import_react28.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), ++ (0, import_react28.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) + ); } - if (owner.kind === "agent") { -@@ -9411,33 +9411,33 @@ function kindLabel(kind, botFocus) { - return labels[kind] ?? kind; - } - function Tabs({ label, value, options, onChange, boxRef }) { -- const names = options.map(([, text]) => text).join("\n"); -+ const names = options.map(([, text2]) => text2).join("\n"); - const glide = useGlide(`${value} - ${names}`, { box: boxRef }); -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( +- const failed = view.error && !view.summarizing ? (0, import_react17.createElement)( ++ const failed = view.error && !view.summarizing ? (0, import_react28.createElement)( "div", - { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, -- (0, import_react24.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- options.map(([id, text, icon2]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon2 ? (0, import_react24.createElement)(icon2) : null, text)) -+ (0, import_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.map(([id, text2, icon3]) => (0, import_react37.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react37.createElement)(icon3) : null, text2)) + { className: "bt-mem-failed", role: "alert" }, +- (0, import_react17.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), +- (0, import_react17.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) ++ (0, import_react28.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), ++ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) + ) : null; +- if (!view.summary) return (0, import_react17.createElement)(import_react17.Fragment, null, failed, failed ? null : (0, import_react17.createElement)(Skeleton, { label: t("Writing the summary…") })); +- return (0, import_react17.createElement)( +- import_react17.Fragment, ++ if (!view.summary) return (0, import_react28.createElement)(import_react28.Fragment, null, failed, failed ? null : (0, import_react28.createElement)(Skeleton, { label: t("Writing the summary…") })); ++ return (0, import_react28.createElement)( ++ import_react28.Fragment, + null, + failed, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "div", + { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, +- summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react17.createElement)("h3", { key: index }, block.text) : (0, import_react17.createElement)("p", { key: index }, block.text)) ++ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react28.createElement)("h3", { key: index }, block.text) : (0, import_react28.createElement)("p", { key: index }, block.text)) + ) ); } - function Stats({ range, total, previous }) { - const shown = useTween(total.total); - const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; -- const cell = (label, value) => (0, import_react24.createElement)("div", { className: "bt-us-stat" }, (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react24.createElement)("span", { className: "bt-us-stat-value" }, value)); -- return (0, import_react24.createElement)( -+ const cell = (label, value) => (0, import_react37.createElement)("div", { className: "bt-us-stat" }, (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react37.createElement)("span", { className: "bt-us-stat-value" }, value)); -+ return (0, import_react37.createElement)( - "section", - { className: "bt-us-stats", style: { "--i": 0 } }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( +@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { + return groups; + }, []); + function EntryRow({ entry, remove }) { +- const [confirming, setConfirming] = (0, import_react17.useState)(false); +- const [busy, setBusy] = (0, import_react17.useState)(false); +- (0, import_react17.useEffect)(() => { ++ const [confirming, setConfirming] = (0, import_react28.useState)(false); ++ const [busy, setBusy] = (0, import_react28.useState)(false); ++ (0, import_react28.useEffect)(() => { + if (!confirming) return void 0; + const timer = setTimeout(() => setConfirming(false), 3e3); + return () => clearTimeout(timer); +@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) { + setBusy(true); + if (!await remove(entry)) setBusy(false); + }; +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "li", + { className: "bt-mem-entry", "data-busy": busy || void 0 }, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( "div", - { className: "bt-us-stat bt-us-stat-main" }, -- (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), -+ (0, import_react37.createElement)( - "span", - { className: "bt-us-stat-value" }, - compact(shown), -- (0, import_react24.createElement)("small", null, t("tokens")), -- change === null ? null : (0, import_react24.createElement)( -+ (0, import_react37.createElement)("small", null, t("tokens")), -+ change === null ? null : (0, import_react37.createElement)( - "span", - { className: "bt-us-delta", title: compareText(range) }, - `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +9455,83 @@ function niceTop(max) { - return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); + { className: "bt-mem-entry-copy" }, +- (0, import_react17.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), +- meta ? (0, import_react17.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null ++ (0, import_react28.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), ++ meta ? (0, import_react28.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null + ), +- (0, import_react17.createElement)("button", { ++ (0, import_react28.createElement)("button", { + type: "button", + className: "bt-mem-remove", + "data-confirm": confirming || void 0, +@@ -7512,7 +7512,7 @@ function EntryRow({ entry, remove }) { + "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), + title: confirming ? void 0 : t("Remove from memory"), + onClick: click +- }, confirming ? t("Remove") : (0, import_react17.createElement)(TrashIcon)) ++ }, confirming ? t("Remove") : (0, import_react28.createElement)(TrashIcon)) + ); } - function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { -- const [hover, setHover] = (0, import_react24.useState)(null); -+ const [hover, setHover] = (0, import_react37.useState)(null); - const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); - const count = columns.length; - const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); - const tipOf = new Map(tipSeries.map((entry) => [entry.key, entry])); - const tip = hover === null ? null : columns[hover]; - const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - "section", - { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( + function Entries({ bot, view, remove }) { +@@ -7521,82 +7521,82 @@ function Entries({ bot, view, remove }) { + { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, + { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } + ]; +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + { className: "bt-mem-entries" }, +- sections.map((section) => (0, import_react17.createElement)( ++ sections.map((section) => (0, import_react28.createElement)( + "section", + { key: section.scope, className: "bt-mem-section" }, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "div", + { className: "bt-mem-section-head" }, +- (0, import_react17.createElement)("h3", null, section.title), +- section.hint ? (0, import_react17.createElement)("span", null, section.hint) : null, +- (0, import_react17.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) ++ (0, import_react28.createElement)("h3", null, section.title), ++ section.hint ? (0, import_react28.createElement)("span", null, section.hint) : null, ++ (0, import_react28.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) + ), +- groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react17.createElement)( +- import_react17.Fragment, ++ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react28.createElement)( ++ import_react28.Fragment, + { key: group.key }, +- group.topic ? (0, import_react17.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, +- (0, import_react17.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react17.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) +- )) : (0, import_react17.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) ++ group.topic ? (0, import_react28.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, ++ (0, import_react28.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react28.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) ++ )) : (0, import_react28.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) + )) + ); + } + function changeLine(change) { +- const text = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; +- if (!change.ok) return t("Not changed: {text} ({reason})", { text, reason: hostText(change.error ?? "") }); +- if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text }) : t("Removed: {text}", { text }); +- if (change.op === "edit") return t("Updated: {text}", { text }); +- return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); ++ const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; ++ if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); ++ if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); ++ if (change.op === "edit") return t("Updated: {text}", { text: text2 }); ++ return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 }); + } + function Reply({ answer, dismiss }) { +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + { className: "bt-mem-reply", role: "status" }, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( "div", - { className: "bt-us-card-head" }, -- (0, import_react24.createElement)("h3", null, t("Tokens over time"), (0, import_react24.createElement)("span", null, unitLabel(range.unit))), -- onBy ? (0, import_react24.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null -+ (0, import_react37.createElement)("h3", null, t("Tokens over time"), (0, import_react37.createElement)("span", null, unitLabel(range.unit))), -+ onBy ? (0, import_react37.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null + { className: "bt-mem-reply-head" }, +- (0, import_react17.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), +- answer.pending ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react17.createElement)(CloseIcon)) ++ (0, import_react28.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), ++ answer.pending ? null : (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react28.createElement)(CloseIcon)) ), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( +- answer.pending ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react17.createElement)( +- import_react17.Fragment, ++ answer.pending ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react28.createElement)( ++ import_react28.Fragment, + null, +- answer.reply ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, +- answer.changes.length > 0 ? (0, import_react17.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react17.createElement)( ++ answer.reply ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, ++ answer.changes.length > 0 ? (0, import_react28.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react28.createElement)( + "li", + { key: index, "data-ok": change.ok || void 0 }, +- change.ok ? (0, import_react17.createElement)(CheckIcon) : (0, import_react17.createElement)(CloseIcon), +- (0, import_react17.createElement)("span", null, changeLine(change)) ++ change.ok ? (0, import_react28.createElement)(CheckIcon) : (0, import_react28.createElement)(CloseIcon), ++ (0, import_react28.createElement)("span", null, changeLine(change)) + ))) : null + ) + ); + } + function useMemoryView(botId, actions) { +- const [view, setView] = (0, import_react17.useState)(null); +- const [error, setError] = (0, import_react17.useState)(""); +- const timer = (0, import_react17.useRef)(0); +- const latest = (0, import_react17.useRef)(0); +- const alive = (0, import_react17.useRef)(true); +- const show = (0, import_react17.useCallback)((next) => { ++ const [view, setView] = (0, import_react28.useState)(null); ++ const [error, setError] = (0, import_react28.useState)(""); ++ const timer = (0, import_react28.useRef)(0); ++ const latest = (0, import_react28.useRef)(0); ++ const alive = (0, import_react28.useRef)(true); ++ const show = (0, import_react28.useCallback)((next) => { + setView(next); + setError(""); + noteCount(botId, next.entries.length); + }, [botId]); +- const load = (0, import_react17.useCallback)(async (flags = {}) => { ++ const load = (0, import_react28.useCallback)(async (flags = {}) => { + clearTimeout(timer.current); + const ticket = ++latest.current; + try { + const next = await actions.memoryView(botId, flags); + if (!alive.current || ticket !== latest.current) return; + show(next); +- if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS2); ++ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5); + } catch (failure) { + if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure)); + } + }, [botId]); +- (0, import_react17.useEffect)(() => { ++ (0, import_react28.useEffect)(() => { + alive.current = true; + void load({ summarize: true }); + return () => { +@@ -7606,9 +7606,9 @@ function useMemoryView(botId, actions) { + }, [botId]); + return { view, error, load, show }; + } +-function useNow(every) { +- const [now, setNow] = (0, import_react17.useState)(Date.now()); +- (0, import_react17.useEffect)(() => { ++function useNow3(every) { ++ const [now, setNow] = (0, import_react28.useState)(Date.now()); ++ (0, import_react28.useEffect)(() => { + const id = setInterval(() => setNow(Date.now()), every); + return () => clearInterval(id); + }, [every]); +@@ -7616,18 +7616,18 @@ function useNow(every) { + } + function MemoryDialog({ bot, actions, leaving = false }) { + const { view, error, load, show } = useMemoryView(bot.id, actions); +- const [page, setPage] = (0, import_react17.useState)("summary"); +- const [menu, setMenu] = (0, import_react17.useState)(false); ++ const [page, setPage] = (0, import_react28.useState)("summary"); ++ const [menu, setMenu] = (0, import_react28.useState)(false); + const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); +- const [text, setText] = (0, import_react17.useState)(""); +- const [answer, setAnswer] = (0, import_react17.useState)(null); +- const menuRef = (0, import_react17.useRef)(null); +- const dotsRef = (0, import_react17.useRef)(null); +- const inputRef = (0, import_react17.useRef)(null); +- const now = useNow(3e4); +- const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); ++ const [text2, setText] = (0, import_react28.useState)(""); ++ const [answer, setAnswer] = (0, import_react28.useState)(null); ++ const menuRef = (0, import_react28.useRef)(null); ++ const dotsRef = (0, import_react28.useRef)(null); ++ const inputRef = (0, import_react28.useRef)(null); ++ const now = useNow3(3e4); ++ const close = (0, import_react28.useCallback)(() => actions.closeMemory(), []); + useEscape(close); +- (0, import_react17.useEffect)(() => { ++ (0, import_react28.useEffect)(() => { + if (!menu) return void 0; + const onDown = (event) => { + if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; +@@ -7671,7 +7671,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { + }; + const ask = async (event) => { + event.preventDefault(); +- const asked = text.trim(); ++ const asked = text2.trim(); + if (asked === "" || answer?.pending) return; + setText(""); + setAnswer({ asked, pending: true }); +@@ -7689,65 +7689,65 @@ function MemoryDialog({ bot, actions, leaving = false }) { + const when3 = view?.summary ? ago(view.summary.at, now) : null; + const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; + const busy = answer?.pending === true; +- return (0, import_react17.createElement)( ++ return (0, import_react28.createElement)( + "div", + { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, +- (0, import_react17.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), ++ (0, import_react28.createElement)( "div", - { className: "bt-us-chart-body" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( + { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( "div", - { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, -- [1, 0.5].map((level) => (0, import_react24.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react24.createElement)("span", null, compact(top * level)))), -- (0, import_react24.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react24.createElement)("span", null, "0")), -- (0, import_react24.createElement)( -+ [1, 0.5].map((level) => (0, import_react37.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react37.createElement)("span", null, compact(top * level)))), -+ (0, import_react37.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react37.createElement)("span", null, "0")), -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-cols" }, -- columns.map((column, at) => (0, import_react24.createElement)("div", { -+ columns.map((column, at) => (0, import_react37.createElement)("div", { - key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, - className: "bt-us-col", - "data-on": hover === at ? "" : void 0, - onMouseEnter: () => setHover(at) -- }, column.total > 0 ? (0, import_react24.createElement)( -+ }, column.total > 0 ? (0, import_react37.createElement)( - "div", - { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, -- column.parts.map(([key, value]) => value > 0 ? (0, import_react24.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) -- ) : (0, import_react24.createElement)("div", { className: "bt-us-stub" }))) -+ column.parts.map(([key, value]) => value > 0 ? (0, import_react37.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) -+ ) : (0, import_react37.createElement)("div", { className: "bt-us-stub" }))) - ), -- tip ? (0, import_react24.createElement)( -+ tip ? (0, import_react37.createElement)( + { className: "bt-mem-head" }, +- page === "entries" ? (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react17.createElement)(ChevronLeftIcon)) : null, +- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), +- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), +- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }), +- (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)), +- (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)), +- menuShown ? (0, import_react17.createElement)( ++ page === "entries" ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react28.createElement)(ChevronLeftIcon)) : null, ++ (0, import_react28.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), ++ (0, import_react28.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), ++ (0, import_react28.createElement)("span", { className: "bt-mem-head-gap" }), ++ (0, import_react28.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react28.createElement)(DotsIcon)), ++ (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react28.createElement)(CloseIcon)), ++ menuShown ? (0, import_react28.createElement)( "div", - { className: "bt-us-tip", style: tipSide }, -- (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, tip.title), -- (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), -- tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, tip.title), -+ (0, import_react37.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), -+ tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react37.createElement)( - "div", - { key, className: "bt-us-tip-row" }, -- (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), -- (0, import_react24.createElement)("span", null, tipOf.get(key)?.name), -- (0, import_react24.createElement)("b", null, compact(value)) -+ (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), -+ (0, import_react37.createElement)("span", null, tipOf.get(key)?.name), -+ (0, import_react37.createElement)("b", null, compact(value)) - )) + { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "button", + { type: "button", role: "menuitem", onClick: openEntries }, +- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("All entries")), +- (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, String(count)) ++ (0, import_react28.createElement)("span", { className: "bt-menu-label" }, t("All entries")), ++ (0, import_react28.createElement)("span", { className: "bt-menu-hint" }, String(count)) + ), +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "button", + { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, +- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) ++ (0, import_react28.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) + ) ) : null ), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( "div", - { className: "bt-us-ticks", "aria-hidden": true }, - // A preset counts its labels back from now; a picked span forward from its first day. -- columns.map((column, at) => (0, import_react24.createElement)( -+ columns.map((column, at) => (0, import_react37.createElement)( - "span", - { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, - (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" - )) + { className: "bt-mem-body" }, +- error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, +- view === null ? error ? null : (0, import_react17.createElement)(Skeleton) : page === "entries" ? (0, import_react17.createElement)(Entries, { bot, view, remove }) : (0, import_react17.createElement)(Summary, { bot, view, regenerate }) ++ error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, ++ view === null ? error ? null : (0, import_react28.createElement)(Skeleton) : page === "entries" ? (0, import_react28.createElement)(Entries, { bot, view, remove }) : (0, import_react28.createElement)(Summary, { bot, view, regenerate }) + ), +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "div", + { className: "bt-mem-foot" }, +- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, +- (0, import_react17.createElement)( ++ answer ? (0, import_react28.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, ++ (0, import_react28.createElement)( + "form", + { className: "bt-mem-ask", onSubmit: ask }, +- (0, import_react17.createElement)("input", { ++ (0, import_react28.createElement)("input", { + ref: inputRef, + className: "bt-mem-input", +- value: text, ++ value: text2, + maxLength: 2e3, + disabled: view === null, + placeholder: t("Ask or update"), + "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), + onChange: (event) => setText(event.target.value) + }), +- (0, import_react17.createElement)( ++ (0, import_react28.createElement)( + "button", +- { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, +- busy ? (0, import_react17.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react17.createElement)(ArrowUpIcon) ++ { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") }, ++ busy ? (0, import_react28.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react28.createElement)(ArrowUpIcon) + ) + ) ) - ), -- series.length > 1 || series[0]?.pick ? (0, import_react24.createElement)( -+ series.length > 1 || series[0]?.pick ? (0, import_react37.createElement)( - "div", - { className: "bt-us-legend" }, -- series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", { -+ series.map((entry) => (0, import_react37.createElement)(entry.pick ? "button" : "span", { - key: entry.key, - className: "bt-us-key", - ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} -- }, (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) -+ }, (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) - ) : null +@@ -7755,8 +7755,40 @@ function MemoryDialog({ bot, actions, leaving = false }) { ); } --function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick }) { -+function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick }) { - const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - onClick ? "button" : "div", - { - className: "bt-us-row", -@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick - title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), - ...onClick ? { type: "button", onClick } : {} - }, -- glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, -- (0, import_react24.createElement)( -+ glyph ? (0, import_react37.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, -+ (0, import_react37.createElement)( - "span", - { className: "bt-us-row-body" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "span", - { className: "bt-us-row-line" }, -- (0, import_react24.createElement)("span", { className: "bt-us-row-name" }, name), -- sub ? (0, import_react24.createElement)("span", { className: "bt-us-row-sub" }, sub) : null -+ (0, import_react37.createElement)("span", { className: "bt-us-row-name" }, name), -+ sub ? (0, import_react37.createElement)("span", { className: "bt-us-row-sub" }, sub) : null - ), -- (0, import_react24.createElement)("span", { className: "bt-us-track" }, (0, import_react24.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) -+ (0, import_react37.createElement)("span", { className: "bt-us-track" }, (0, import_react37.createElement)("span", { style: { width: `${width}%`, background: fill3 } })) - ), -- (0, import_react24.createElement)("span", { className: "bt-us-row-num" }, (0, import_react24.createElement)("b", null, compact(sum.total)), (0, import_react24.createElement)("small", null, `${percent(sum.total, whole)}%`)) -+ (0, import_react37.createElement)("span", { className: "bt-us-row-num" }, (0, import_react37.createElement)("b", null, compact(sum.total)), (0, import_react37.createElement)("small", null, `${percent(sum.total, whole)}%`)) + ++// src/client/standing-approvals.js ++var import_react29 = require("react"); ++var standingOf = (target, roster) => { ++ const me = roster?.self?.memberId; ++ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); ++}; ++function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card2 }) { ++ const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; ++ if (grants.length === 0) return null; ++ const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); ++ return (0, import_react29.createElement)( ++ Card2, ++ { title: t("Always allowed") }, ++ grants.map((grant) => (0, import_react29.createElement)( ++ "div", ++ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, ++ (0, import_react29.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), ++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) ++ ), ++ (0, import_react29.createElement)("button", { ++ type: "button", ++ className: "bt-soft bt-soft-sm", ++ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), ++ onClick: () => run(actions.removeStandingApproval(scope, target.id, grant.kind)) ++ }, t("Remove")) ++ )) ++ ); ++} ++ + // src/client/bot-settings.js +-var import_react18 = require("react"); ++var import_react30 = require("react"); + var capitalize = (word) => word[0].toUpperCase() + word.slice(1); + async function avatarImage(file) { + if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); +@@ -7780,8 +7780,8 @@ async function avatarImage(file) { + throw new Error(t("That image is too large")); + } + function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { +- const [draft, setDraft] = (0, import_react18.useState)(null); +- const reverting = (0, import_react18.useRef)(false); ++ const [draft, setDraft] = (0, import_react30.useState)(null); ++ const reverting = (0, import_react30.useRef)(false); + const commit = () => { + const next = draft?.trim(); + setDraft(null); +@@ -7792,7 +7792,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu + if (next === void 0 || next === (value ?? "") || required && next === "") return; + void onSave(next); + }; +- return (0, import_react18.createElement)(multiline ? "textarea" : "input", { ++ return (0, import_react30.createElement)(multiline ? "textarea" : "input", { + className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", + value: draft ?? value ?? "", + placeholder, +@@ -7813,29 +7813,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu + }); + } + function Row({ label, hint, stack = false, children }) { +- return (0, import_react18.createElement)( ++ return (0, import_react30.createElement)( + "label", + { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react30.createElement)("span", { className: "bt-set-hint" }, hint) : null), + children ); } - function ListCard({ title, count, index, children }) { -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - "section", - { className: "bt-us-card", style: { "--i": index } }, -- (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, title, count === void 0 ? null : (0, import_react24.createElement)("span", null, count))), -- (0, import_react24.createElement)("div", { className: "bt-us-rows" }, children) -+ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, title, count === void 0 ? null : (0, import_react37.createElement)("span", null, count))), -+ (0, import_react37.createElement)("div", { className: "bt-us-rows" }, children) + function Card({ title, children }) { +- return (0, import_react18.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react18.createElement)("h3", null, title) : null, (0, import_react18.createElement)("div", { className: "bt-set-card" }, children)); ++ return (0, import_react30.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react30.createElement)("h3", null, title) : null, (0, import_react30.createElement)("div", { className: "bt-set-card" }, children)); + } + function Switch({ on, label, disabled, onChange }) { +- return (0, import_react18.createElement)( ++ return (0, import_react30.createElement)( + "button", + { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, +- (0, import_react18.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ++ (0, import_react30.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ); } - function Profile({ face, share, range }) { -- const arc = Math.max(0, Math.min(1, share)) * RING; -- return (0, import_react24.createElement)( -+ const arc = Math.max(0, Math.min(1, share)) * RING2; -+ return (0, import_react37.createElement)( - "section", - { className: "bt-us-profile" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "span", - { className: "bt-us-ring" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "svg", - { viewBox: "0 0 96 96", "aria-hidden": true }, -- face.stops ? (0, import_react24.createElement)("defs", null, (0, import_react24.createElement)( -+ face.stops ? (0, import_react37.createElement)("defs", null, (0, import_react37.createElement)( - "linearGradient", - { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, -- face.stops.map(([offset, color]) => (0, import_react24.createElement)("stop", { key: offset, offset, stopColor: color })) -+ face.stops.map(([offset, color]) => (0, import_react37.createElement)("stop", { key: offset, offset, stopColor: color })) - )) : null, -- (0, import_react24.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), -- arc > 0 ? (0, import_react24.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING}` } }) : null -+ (0, import_react37.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), -+ arc > 0 ? (0, import_react37.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null - ), -- (0, import_react24.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) -+ (0, import_react37.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) - ), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-profile-copy" }, -- (0, import_react24.createElement)("h3", null, face.name), -- face.sub ? (0, import_react24.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, -- (0, import_react24.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) -+ (0, import_react37.createElement)("h3", null, face.name), -+ face.sub ? (0, import_react37.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, -+ (0, import_react37.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) - ) +-function SwitchRow({ label, hint, on, disabled, onChange }) { +- return (0, import_react18.createElement)( ++function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { ++ return (0, import_react30.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), +- (0, import_react18.createElement)(Switch, { on, label, disabled, onChange }) ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react30.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ (0, import_react30.createElement)(Switch, { on, label, disabled, onChange }) + ); + } + function modelHint(bot, roster) { +@@ -7846,22 +7846,22 @@ function modelHint(bot, roster) { + return t("Applies from the next message."); + } + function ShapeRing({ shape, size }) { +- const maskId = (0, import_react18.useRef)(""); ++ const maskId = (0, import_react30.useRef)(""); + if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; + const def = shapeOf(shape); + const unit = def.size / size; + const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; +- return (0, import_react18.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); ++ return (0, import_react30.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); + } + function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { + const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; + if (!shapes.includes(look.shape)) shapes.push(look.shape); + const rows = []; + for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); +- return (0, import_react18.createElement)( +- import_react18.Fragment, ++ return (0, import_react30.createElement)( ++ import_react30.Fragment, + null, +- (0, import_react18.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react18.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react18.createElement)("button", { ++ (0, import_react30.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react30.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react30.createElement)("button", { + key: id, + type: "button", + className: "bt-shape", +@@ -7869,8 +7869,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { + title: t(shapeOf(id).label), + "aria-pressed": !hasPhoto && look.shape === id, + onClick: () => onShape(id) +- }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), +- (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", { ++ }, (0, import_react30.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react30.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), ++ (0, import_react30.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react30.createElement)("button", { + key: color, + type: "button", + className: "bt-color", +@@ -7878,19 +7878,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { + title: t(capitalize(color)), + "aria-pressed": look.color === color, + onClick: () => onColor(color) +- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } })))) ++ }, (0, import_react30.createElement)("span", { style: { background: paintCss(color) } })))) ); } - var monthOf = (ms) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth(), 1); - var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); - function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { -- const box = (0, import_react24.useRef)(null); -+ const box = (0, import_react37.useRef)(null); - const todayMs = hourMs(today); - const thisMonth = monthOf(todayMs); -- const [right, setRight] = (0, import_react24.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); -- const [anchor, setAnchor] = (0, import_react24.useState)(null); -- const [hover, setHover] = (0, import_react24.useState)(null); -- (0, import_react24.useEffect)(() => { -+ const [right, setRight] = (0, import_react37.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); -+ const [anchor, setAnchor] = (0, import_react37.useState)(null); -+ const [hover, setHover] = (0, import_react37.useState)(null); -+ (0, import_react37.useEffect)(() => { + function AvatarEditor({ bot, actions, run, onClose }) { + const look = lookOf(bot); +- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot"); +- const [over, setOver] = (0, import_react18.useState)(false); +- const ref = (0, import_react18.useRef)(null); +- const fileRef = (0, import_react18.useRef)(null); ++ const [tab, setTab] = (0, import_react30.useState)(look.image ? "upload" : "bot"); ++ const [over, setOver] = (0, import_react30.useState)(false); ++ const ref = (0, import_react30.useRef)(null); ++ const fileRef = (0, import_react30.useRef)(null); + const upload = (file) => { + if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); + }; +- (0, import_react18.useEffect)(() => { ++ (0, import_react30.useEffect)(() => { const onDown = (event) => { - if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); + if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; + onClose(); +@@ -7908,7 +7908,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { + window.removeEventListener("keydown", onKey, true); }; -@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { - const [from, to] = [anchor, key].sort(); - onPick(from, to); - }; -- const weekdays = (0, import_react24.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); -+ const weekdays = (0, import_react37.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); - const month = (start, side) => { - const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); - const lead = (new Date(start).getUTCDay() + 6) % 7; -- const cells = Array.from({ length: lead }, (_, at) => (0, import_react24.createElement)("span", { key: `b${at}` })); -+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react37.createElement)("span", { key: `b${at}` })); - for (let day = 1; day <= length; day += 1) { - const ms = start + (day - 1) * DAY_MS; - const key = dayKey(ms); - const weekday = (lead + day - 1) % 7; - const flag = (on) => on ? "" : void 0; -- cells.push((0, import_react24.createElement)("button", { -+ cells.push((0, import_react37.createElement)("button", { - key, - type: "button", - className: "bt-us-day", -@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { - "data-has": flag(active.has(key)), - onMouseEnter: () => setHover(key), - onClick: () => press(key) -- }, (0, import_react24.createElement)("b", null, day))); -+ }, (0, import_react37.createElement)("b", null, day))); - } -- const nav = side === "left" ? (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react24.createElement)(ChevronLeftIcon)) : (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react24.createElement)(ChevronRightIcon)); -- return (0, import_react24.createElement)( -+ const nav = side === "left" ? (0, import_react37.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react37.createElement)(ChevronLeftIcon)) : (0, import_react37.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react37.createElement)(ChevronRightIcon)); -+ return (0, import_react37.createElement)( - "div", - { key: start, className: "bt-us-month" }, -- (0, import_react24.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react24.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), -- (0, import_react24.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react24.createElement)("span", { key: at }, name))), -- (0, import_react24.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) -+ (0, import_react37.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react37.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), -+ (0, import_react37.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react37.createElement)("span", { key: at }, name))), -+ (0, import_react37.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) - ); - }; - const shortcuts = [ -@@ -9671,30 +9671,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { - ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] - ]; - const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( + }, [onClose]); +- (0, import_react18.useEffect)(() => { ++ (0, import_react30.useEffect)(() => { + if (tab !== "upload") return void 0; + const onPaste = (event) => { + const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); +@@ -7922,32 +7922,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { + }, [tab, bot]); + const hasPhoto = Boolean(look.image); + const glide = useGlide(tab); +- return (0, import_react18.createElement)( ++ return (0, import_react30.createElement)( "div", - { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-pick-quick" }, -- shortcuts.map(([id, label, from, to]) => (0, import_react24.createElement)("button", { -+ shortcuts.map(([id, label, from, to]) => (0, import_react37.createElement)("button", { - key: id, - type: "button", - "aria-pressed": span?.from === from && span?.to === to, - onClick: () => onPick(from, to) - }, label)) - ), -- (0, import_react24.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-pick-foot", role: "status" }, -- (0, import_react24.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), -- low === null ? null : (0, import_react24.createElement)("span", null, (0, import_react24.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) -+ (0, import_react37.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), -+ low === null ? null : (0, import_react37.createElement)("span", null, (0, import_react37.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) - ) - ); - } - function Heat({ view, keep, paint, index }) { -- const [hover, setHover] = (0, import_react24.useState)(null); -+ const [hover, setHover] = (0, import_react37.useState)(null); - const today = hourMs(`${view.now.slice(0, 10)} 00`); - const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; - const from = dayKey(first); -@@ -9710,13 +9710,13 @@ function Heat({ view, keep, paint, index }) { - for (let week = 0; week < HEAT_WEEKS; week += 1) { - const monday = first + week * 7 * DAY_MS; - if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { -- months.push((0, import_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); -+ months.push((0, import_react37.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); - } - for (let day = 0; day < 7; day += 1) { - const ms = monday + day * DAY_MS; - const value = daily.get(dayKey(ms)) ?? 0; - const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; -- cells.push((0, import_react24.createElement)("span", { -+ cells.push((0, import_react37.createElement)("span", { - key: ms, - className: "bt-us-cell", - style: { "--w": week }, -@@ -9724,7 +9724,7 @@ function Heat({ view, keep, paint, index }) { - "data-future": ms > today ? "" : void 0, - "data-today": ms === today ? "" : void 0, - onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) -- }, level > 0 ? (0, import_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); -+ }, level > 0 ? (0, import_react37.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); - } - } - const tipSide = hover ? { -@@ -9733,61 +9733,61 @@ function Heat({ view, keep, paint, index }) { - } : null; - const active = [...daily.values()].filter((value) => value > 0).length; - const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - "section", - { className: "bt-us-card bt-us-heat", style: { "--i": index } }, -- (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, t("Activity")), (0, import_react24.createElement)("span", { className: "bt-us-head-note" }, summary)), -- (0, import_react24.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, t("Activity")), (0, import_react37.createElement)("span", { className: "bt-us-head-note" }, summary)), -+ (0, import_react37.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), -+ (0, import_react37.createElement)( + { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( "div", - { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, -- (0, import_react24.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), -- hover ? (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), -+ hover ? (0, import_react37.createElement)( + { className: "bt-avedit-top" }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( "div", - { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, -- (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), -- (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) -+ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), -+ (0, import_react37.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) - ) : null + { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, +- (0, import_react18.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react18.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) ++ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react30.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) + ), +- (0, import_react18.createElement)("button", { ++ (0, import_react30.createElement)("button", { + type: "button", + className: "bt-avedit-tab bt-avedit-reset", + title: t("Reset character to default"), + onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) + }, t("Reset")) ), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-heat-scale", "aria-hidden": true }, - t("Light"), -- levels.map((level) => (0, import_react24.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react24.createElement)("i", { style: { background: soften(paint, level) } }))), -+ levels.map((level) => (0, import_react37.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react37.createElement)("i", { style: { background: soften(paint, level) } }))), - t("Heavy") - ) - ); - } - function Skeleton2() { -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - "div", - { className: "bt-us", "aria-busy": true }, -- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), -+ (0, import_react37.createElement)( +- tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, { ++ tab === "bot" ? (0, import_react30.createElement)(CharacterGrid, { + look, + hasPhoto, + onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), + onColor: (color) => run(actions.updateBot(bot.id, { color })) + }) : null, +- tab === "upload" ? (0, import_react18.createElement)( ++ tab === "upload" ? (0, import_react30.createElement)( "div", - { className: "bt-us-sheet bt-us-skel-sheet" }, -- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), -- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), -+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), -+ (0, import_react37.createElement)( + { + className: "bt-dropzone", +@@ -7963,15 +7963,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { + upload(event.dataTransfer.files?.[0]); + } + }, +- hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null, +- (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")), +- (0, import_react18.createElement)( ++ hasPhoto ? (0, import_react30.createElement)("img", { src: look.image, alt: "" }) : null, ++ (0, import_react30.createElement)("div", null, t("Drag, drop, or paste an image")), ++ (0, import_react30.createElement)( "div", - { className: "bt-us-pair" }, -- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), -- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) -+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), -+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) - ) + { className: "bt-actions", style: { justifyContent: "center" } }, +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), +- hasPhoto ? (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null ++ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), ++ hasPhoto ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null + ), +- (0, import_react18.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { ++ (0, import_react30.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { + const file = event.target.files?.[0]; + event.target.value = ""; + upload(file); +@@ -7982,26 +7982,27 @@ function AvatarEditor({ bot, actions, run, onClose }) { + function MemoryCard({ bot, actions }) { + const count = useMemoryCount(bot.id, actions); + const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); +- return (0, import_react18.createElement)( ++ return (0, import_react30.createElement)( + Card, + { title: t("Memory") }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + "button", + { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label)), +- (0, import_react18.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react18.createElement)(ChevronRightIcon)) ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label)), ++ (0, import_react30.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react30.createElement)(ChevronRightIcon)) ) ); } - var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null; - function UsagePage({ roster, actions, focusId }) { -- const [view, setView] = (0, import_react24.useState)(null); -- const [error, setError] = (0, import_react24.useState)(""); -- const [rangeId, setRangeId] = (0, import_react24.useState)("7d"); -- const [span, setSpan] = (0, import_react24.useState)(null); -- const [picking, setPicking] = (0, import_react24.useState)(false); -- const [by, setBy] = (0, import_react24.useState)("bot"); -- const tabs = (0, import_react24.useRef)(null); -+ const [view, setView] = (0, import_react37.useState)(null); -+ const [error, setError] = (0, import_react37.useState)(""); -+ const [rangeId, setRangeId] = (0, import_react37.useState)("7d"); -+ const [span, setSpan] = (0, import_react37.useState)(null); -+ const [picking, setPicking] = (0, import_react37.useState)(false); -+ const [by, setBy] = (0, import_react37.useState)("bot"); -+ const tabs = (0, import_react37.useRef)(null); - const focus = focusOf(focusId); -- (0, import_react24.useEffect)(() => { -+ (0, import_react37.useEffect)(() => { - let stopped = false; - let timer; - const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) { - const value = await actions.usage(); - await resized; - if (stopped) return; -- (0, import_react24.startTransition)(() => setView(value)); -+ (0, import_react37.startTransition)(() => setView(value)); - setError(""); - timer = setTimeout(load, value.scanning ? 3e3 : 2e4); - } catch (failure) { -@@ -9814,7 +9814,7 @@ function UsagePage({ roster, actions, focusId }) { - }, []); - const today = view?.now.slice(0, 10); - const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); -- const shaped = (0, import_react24.useMemo)(() => { -+ const shaped = (0, import_react37.useMemo)(() => { - if (!view) return null; - const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); - const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +9823,7 @@ function UsagePage({ roster, actions, focusId }) { - const models2 = view.models.map((model, at) => { - const name = model.name || model.model || t("Unknown model"); - const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; -- return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react24.createElement)(ModelGlyph, { name, paint, size }) }; -+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react37.createElement)(ModelGlyph, { name, paint, size }) }; - }); - const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; - const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +9871,23 @@ function UsagePage({ roster, actions, focusId }) { - }); - return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; - }, [view, rangeId, span, by, focusId, roster]); -- const root = (0, import_react24.useRef)(null); -- (0, import_react24.useEffect)(() => { -+ const root = (0, import_react37.useRef)(null); -+ (0, import_react37.useEffect)(() => { - root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); - }, [focusId]); - if (!view) { -- return error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react24.createElement)(Skeleton2); -+ return error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react37.createElement)(Skeleton2); - } - if (view.rows.length === 0) { -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( + function BotSettingsForm({ bot, roster, actions, run }) { +- const [editing, setEditing] = (0, import_react18.useState)(false); +- const [confirming, setConfirming] = (0, import_react18.useState)(false); +- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []); +- (0, import_react18.useEffect)(() => { ++ const [editing, setEditing] = (0, import_react30.useState)(false); ++ const [confirming, setConfirming] = (0, import_react30.useState)(false); ++ const closeEditor = (0, import_react30.useCallback)(() => setEditing(false), []); ++ (0, import_react30.useEffect)(() => { + if (!confirming) return void 0; + const timer = setTimeout(() => setConfirming(false), 4e3); + return () => clearTimeout(timer); + }, [confirming]); ++ if (bot.remote) return null; + const isMainBot = isMainOf(roster, bot.id); + const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; + const isTile = roster.mainBotId === bot.id; +@@ -8011,149 +8011,812 @@ function BotSettingsForm({ bot, roster, actions, run }) { + if (confirming) void run(actions.deleteBot(bot.id)); + else setConfirming(true); + }; +- (0, import_react18.useEffect)(() => { ++ (0, import_react30.useEffect)(() => { + void actions.refreshModels?.(); + }, [bot.id]); +- return (0, import_react18.createElement)( ++ return (0, import_react30.createElement)( + "div", + { className: "bt-bs" }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( "div", - { className: "bt-us" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-empty" }, -- (0, import_react24.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), -- (0, import_react24.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), -- (0, import_react24.createElement)("span", null, t("Token usage shows up here after a model is called.")) -+ (0, import_react37.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), -+ (0, import_react37.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), -+ (0, import_react37.createElement)("span", null, t("Token usage shows up here after a model is called.")) - ) - ); - } -@@ -9912,7 +9912,7 @@ function UsagePage({ roster, actions, focusId }) { - ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] - ]; - const whole = part.total.total; -- const ownerRow = ([at, sum], index, total = whole) => (0, import_react24.createElement)(Row2, { -+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react37.createElement)(Row3, { - key: `o${at}`, - kind: faces[at].kind, - glyph: faces[at].glyph(32), -@@ -9924,7 +9924,7 @@ function UsagePage({ roster, actions, focusId }) { - fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), - onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) - }); -- const modelRow = ([at, sum], index) => (0, import_react24.createElement)(Row2, { -+ const modelRow = ([at, sum], index) => (0, import_react37.createElement)(Row3, { - key: `m${at}`, - kind: "model", - glyph: models[at].glyph(32), -@@ -9936,30 +9936,30 @@ function UsagePage({ roster, actions, focusId }) { - fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), - onClick: () => pick2({ kind: "model", key: view.models[at].key }) - }); -- const nothing = (0, import_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); -+ const nothing = (0, import_react37.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); - let body; - if (face) { - const kinds = ranked(part.kinds); - const usedModels = ranked(part.models); - const owners = ranked(part.owners); - body = [ -- (0, import_react24.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), -+ (0, import_react37.createElement)( + { className: "bt-bs-hero" }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( "div", - { key: "sheet", className: "bt-us-sheet" }, -- (0, import_react24.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), -- (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), -+ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), -+ (0, import_react37.createElement)( - "div", - { key: "pair", className: "bt-us-pair" }, -- ownerAt >= 0 ? (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react24.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), -- (0, import_react24.createElement)( -+ ownerAt >= 0 ? (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react37.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), -+ (0, import_react37.createElement)( - ListCard, - { title: t("What for"), index: 3 }, -- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react24.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing -+ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react37.createElement)(Row3, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing - ) + { className: "bt-avatar-anchor" }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + "button", + { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, +- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), +- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) ++ (0, import_react30.createElement)(BotMark, { bot, size: 64, live: true }), ++ (0, import_react30.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react30.createElement)(PencilIcon)) ), -- (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) -+ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) - ) - ]; - } else { -@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) { - const rest = owners.filter(([at]) => !isBot(at)); - const usedModels = ranked(all.models); - body = [ -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( +- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ++ editing ? (0, import_react30.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null + ), +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( "div", - { key: "sheet", className: "bt-us-sheet" }, -- (0, import_react24.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), -- (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), -+ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), -+ (0, import_react37.createElement)( - "div", - { key: "pair", className: "bt-us-pair" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - ListCard, - { title: t("Bots"), count: bots.length, index: 2 }, - owners.length > 0 ? null : nothing, - bots.map((entry, index) => ownerRow(entry, index)), -- bots.length > 0 && rest.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, -+ bots.length > 0 && rest.length > 0 ? (0, import_react37.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, - rest.map((entry, index) => ownerRow(entry, bots.length + index)) - ), -- (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) -+ (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) - ), -- (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) -+ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) + { className: "bt-bs-who" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), +- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) ++ (0, import_react30.createElement)("span", { className: "bt-bs-name" }, bot.name), ++ (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) ) - ]; - } -- return (0, import_react24.createElement)( -+ return (0, import_react37.createElement)( - "div", - { ref: root, className: "bt-us", key: focusId ?? "all" }, -- (0, import_react24.createElement)( -+ (0, import_react37.createElement)( - "div", - { className: "bt-us-top" }, -- (0, import_react24.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), -- view.scanning ? (0, import_react24.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, -- picking ? (0, import_react24.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null -+ (0, import_react37.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), -+ view.scanning ? (0, import_react37.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, -+ picking ? (0, import_react37.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null ), -- error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, - body - ); - } -@@ -10150,33 +10150,216 @@ button.bt-us-row:hover{background:var(--bt-hover)} - @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} - @keyframes bt-us-fade{from{opacity:0}} - @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} --@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING)}}} -+@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING2)}}} - @keyframes bt-us-pulse{50%{opacity:.25}} - @keyframes bt-us-shine{from{background-position:200% 0}to{background-position:-200% 0}} - @media (prefers-reduced-motion:reduce){.bt-us *,.bt-us *::before{animation:none!important;transition:none!important}} - `; - - // src/client/connectors.js --var import_react25 = require("react"); -+var import_react38 = require("react"); - var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; --var GitHubMark = () => (0, import_react25.createElement)( -+var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; -+var FEISHU_WEBHOOK_URL = "https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot"; -+var APPS = [ -+ ["gmail", "Gmail"], -+ ["googlecalendar", "Google Calendar"], -+ ["googledrive", "Google Drive"], -+ ["googledocs", "Google Docs"], -+ ["googlesheets", "Google Sheets"], -+ ["outlook", "Outlook"], -+ ["slack", "Slack"], -+ ["microsoft_teams", "Microsoft Teams"], -+ ["discord", "Discord"], -+ ["telegram", "Telegram"], -+ ["whatsapp", "WhatsApp"], -+ ["notion", "Notion"], -+ ["linear", "Linear"], -+ ["jira", "Jira"], -+ ["asana", "Asana"], -+ ["trello", "Trello"], -+ ["todoist", "Todoist"], -+ ["airtable", "Airtable"], -+ ["dropbox", "Dropbox"], -+ ["hubspot", "HubSpot"], -+ ["salesforce", "Salesforce"], -+ ["stripe", "Stripe"], -+ ["zoom", "Zoom"], -+ ["calendly", "Calendly"], -+ ["figma", "Figma"], -+ ["twitter", "X (Twitter)"], -+ ["youtube", "YouTube"] +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + Card, + { title: t("Profile") }, +- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), +- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) ++ (0, import_react30.createElement)(Row, { label: t("Name") }, (0, import_react30.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), ++ (0, import_react30.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react30.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) + ), +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + Card, + { title: t("Model") }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), +- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) +- ) ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), ++ (0, import_react30.createElement)(ModelMenu, { bot, roster, actions, run }) ++ ), ++ bot.model ? (0, import_react30.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null + ), +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + Card, + { title: t("Working folder") }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), ++ (0, import_react30.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) + ) + ), +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + Card, + { title: t("Instructions") }, +- (0, import_react18.createElement)( ++ (0, import_react30.createElement)( + Row, + { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, +- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) ++ (0, import_react30.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) + ) + ), +- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), +- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), +- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), +- (0, import_react18.createElement)( ++ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). ++ (0, import_react30.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react30.createElement)(Card, { title: t("Sharing") }, children) }), ++ (0, import_react30.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), ++ (0, import_react30.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), ++ roster.memory === false ? null : (0, import_react30.createElement)(MemoryCard, { bot, actions }), ++ (0, import_react30.createElement)(BotSchedules, { bot, roster, actions }), ++ (0, import_react30.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), ++ (0, import_react30.createElement)( ++ Card, ++ { title: t("Main Bot") }, ++ onlyMain ? (0, import_react30.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row" }, ++ (0, import_react30.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ ), ++ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) ++ ) : (0, import_react30.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) ++ ), ++ // The Main Bot tile is always first and always shown, like in the context menu. ++ isTile ? null : (0, import_react30.createElement)( ++ Card, ++ { title: t("Sidebar") }, ++ (0, import_react30.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), ++ isMainBot ? null : (0, import_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) ++ ), ++ (0, import_react30.createElement)( ++ Card, ++ { title: t("More") }, ++ (0, import_react30.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, ++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), ++ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) ++ ), ++ isMainBot ? null : (0, import_react30.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, ++ (0, import_react30.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), ++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) ++ ), ++ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) ++ ) ++ ) ++ ); ++} ++var BOT_SETTINGS_CSS = ` ++.bt-bs{display:flex;flex-direction:column;gap:18px} ++.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} ++.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} ++.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-bs .bt-avedit{left:0;transform:none} ++.bt-bs-row{min-height:48px} ++label.bt-bs-row{cursor:text} ++.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} ++/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to ++ break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test ++ connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under ++ a label is prose, so it wraps and is never cut: the row grows instead. */ ++.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} ++.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} ++.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} ++.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} ++${MODEL_TEST_CSS} ++.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} ++.bt-bs-input:hover{background:var(--bt-hover)} ++.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} ++.bt-bs-input::placeholder{color:var(--bt-ink-3)} ++.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} ++.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} ++.bt-bs-area:hover{background:var(--bt-active)} ++.bt-bs-danger .bt-bs-label{color:#c21d2e} ++.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} ++.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} ++.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} ++.bt-switch[aria-checked=true]{background:var(--bt-accent)} ++.bt-switch:disabled{opacity:.45;cursor:default} ++.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} ++.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} ++.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} ++.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} ++.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} ++.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} ++@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} ++/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below ++ 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and ++ keeps its own words. Nothing is clipped to fit a width that is not there. */ ++@container bt-panel (max-width:300px){ ++ .bt-bs-row{flex-wrap:wrap;row-gap:6px} ++ .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} ++ .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} ++} ++`; ++ ++// src/client/group-settings.js ++var import_react31 = require("react"); ++var ROOM_MODES = [ ++ ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], ++ ["everyone", "Everyone", "Every member answers in turn, admin first."] +]; -+var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; -+var GitHubMark = () => (0, import_react38.createElement)( - "svg", - { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, -- (0, import_react25.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) -+ (0, import_react38.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) -+); -+var FeishuMark = () => (0, import_react38.createElement)( -+ "svg", -+ { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -+ (0, import_react38.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }), -+ (0, import_react38.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }), -+ (0, import_react38.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" }) - ); --var MARKS = { github: GitHubMark }; --var ABOUT = { github: () => t("Repositories, issues, and pull requests") }; -+var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark }; -+var ABOUT = { -+ github: () => t("Repositories, issues, and pull requests"), -+ composio: () => t("Gmail, Slack, Calendar, Notion and many more apps, through one Composio key"), -+ feishu: () => t("Posts into one Feishu group through a custom bot webhook") -+}; - function statusText2(connector) { - if (connector.state === "connecting") return t("Connecting…"); - if (connector.state === "error") return t("Could not connect: {error}", { error: connector.error ?? "" }); -+ if (connector.state === "ready" && connector.kind === "channel") { -+ return connector.mode === "app" ? t("Connected: app, over the long connection") : t("Connected: custom bot webhook"); ++var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; ++function faceSeatBot(seat, roster) { ++ if (!seat) return void 0; ++ const record = seat.record && typeof seat.record === "object" ? seat.record : {}; ++ const extra = {}; ++ if (typeof record.color === "string" && record.color) extra.color = record.color; ++ if (record.avatar && typeof record.avatar === "object") extra.avatar = record.avatar; ++ if (seat.local && !seat.local.remote) return Object.keys(extra).length ? { ...seat.local, ...extra } : seat.local; ++ const handle = typeof record.handle === "string" && record.handle.trim() ? record.handle : typeof seat.key === "string" && seat.key.startsWith("@") ? seat.key : ""; ++ return lookBotForRemote({ ++ handle, ++ displayName: record.displayName || seat.bot, ++ name: seat.bot, ++ label: record.label, ++ id: record.id || seat.key, ++ ...extra ++ }, roster); ++} ++function faceMemberLook(bot) { ++ if (!bot) return void 0; ++ return bot.remote ? lookAvoidingLocals(bot) : lookOf(bot); ++} ++function faceMarks(person, roster, size) { ++ const marks = []; ++ for (const seat of person?.seats ?? []) { ++ const bot = faceSeatBot(seat, roster); ++ if (!bot) continue; ++ marks.push((0, import_react31.createElement)(BotMark, { key: seat.key, bot, size, live: false })); + } -+ if (connector.state === "ready" && connector.apps?.length > 0) return t("Connected: {apps}", { apps: connector.apps.map(appName).join(", ") }); - if (connector.state === "ready") return connector.account ? t("Connected as {account}", { account: connector.account }) : t("Connected"); - return ABOUT[connector.id]?.() ?? ""; - } --function ConnectorRow({ connector, actions }) { -- const [form, setForm] = (0, import_react25.useState)(false); -- const [token, setToken] = (0, import_react25.useState)(""); -- const [busy, setBusy] = (0, import_react25.useState)(false); -- const [error, setError] = (0, import_react25.useState)(null); -+function CredentialField({ credential, value, onChange, onEnter, autoFocus }) { -+ const label = t(credential.label); -+ if (credential.masked) return (0, import_react38.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus }); -+ return (0, import_react38.createElement)( -+ "label", -+ { className: "bt-q-field" }, -+ (0, import_react38.createElement)("input", { -+ className: "bt-input", -+ type: "text", -+ value, -+ "aria-label": label, -+ placeholder: label, -+ spellCheck: false, -+ autoFocus, -+ onChange: (event) => onChange(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter") onEnter?.(); -+ } -+ }) -+ ); ++ if (marks.length === 0) return [(0, import_react31.createElement)(PersonMark, { key: "person", size, name: person?.name })]; ++ return marks; +} -+function AllowedSenders({ connector, actions, run, busy }) { -+ const [openId, setOpenId] = (0, import_react38.useState)(""); -+ const allowed = connector.allowed ?? []; -+ const refused = connector.inbound?.refusedSenders ?? []; -+ const allow = (id) => void run(actions.connectorAllow(connector.id, id)); -+ return (0, import_react38.createElement)( ++function faceRoomCount(room2, roster) { ++ const people2 = facePeople(room2, roster).length; ++ const seats = faceSeats(room2, roster).length; ++ const persons = people2 === 1 ? t("1 person") : t("{count} people", { count: people2 }); ++ const bots = seats === 1 ? t("1 Bot") : t("{count} Bots", { count: seats }); ++ return `${persons} · ${bots}`; ++} ++var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); ++var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); ++function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { ++ const bots = person.seats.flatMap((seat, index) => { ++ const name = seat.local && onOpenBot ? (0, import_react31.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react31.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); ++ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react31.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; ++ return index === 0 ? marked : [(0, import_react31.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; ++ }); ++ return (0, import_react31.createElement)( + "div", -+ { className: "bt-connector-allowed" }, -+ (0, import_react38.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")), -+ allowed.length === 0 && refused.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null, -+ allowed.map((entry) => (0, import_react38.createElement)( -+ "div", -+ { key: entry.openId, className: "bt-connector-person" }, -+ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block")) -+ )), -+ refused.map((entry) => (0, import_react38.createElement)( -+ "div", -+ { key: entry.openId, className: "bt-connector-person" }, -+ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow")) -+ )), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_react38.createElement)("input", { -+ className: "bt-input", -+ type: "text", -+ value: openId, -+ spellCheck: false, -+ "aria-label": t("Feishu open_id"), -+ placeholder: t("Paste an open_id to allow"), -+ onChange: (event) => setOpenId(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" && openId.trim() !== "") { -+ allow(openId.trim()); -+ setOpenId(""); -+ } -+ } -+ }), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => { -+ allow(openId.trim()); -+ setOpenId(""); -+ } }, t("Allow")) ++ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, ++ (0, import_react31.createElement)( ++ "span", ++ { className: "bt-gs-who bt-face-person" }, ++ (0, import_react31.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), ++ (0, import_react31.createElement)( ++ "span", ++ { className: "bt-face-person-text" }, ++ (0, import_react31.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), ++ (0, import_react31.createElement)("span", { className: "bt-face-bots" }, bots) ++ ) + ) + ); +} -+var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; -+function AppChip({ name, status, onRemove }) { -+ const label = APP_STATUS[status]?.(); -+ return (0, import_react38.createElement)( -+ "button", -+ { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, -+ name, -+ label ? (0, import_react38.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null -+ ); -+} -+function AppPicker({ apps, onChange, search, statuses = {} }) { -+ const [query, setQuery] = (0, import_react38.useState)(""); -+ const [found, setFound] = (0, import_react38.useState)(null); -+ const [failed, setFailed] = (0, import_react38.useState)(null); -+ const names = (0, import_react38.useRef)(new Map(APPS)); -+ const asked = (0, import_react38.useRef)(0); -+ (0, import_react38.useEffect)(() => { -+ const text2 = query.trim(); -+ if (text2 === "" || search === void 0) { -+ setFound(null); -+ setFailed(null); -+ return void 0; ++function FaceLead({ room: room2, roster, pick: pick2 }) { ++ const lead = room2.face?.lead; ++ const [open, setOpen] = (0, import_react31.useState)(false); ++ const [busy, setBusy] = (0, import_react31.useState)(false); ++ const [error, setError] = (0, import_react31.useState)(""); ++ const seats = faceSeats(room2, roster); ++ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); ++ if (!current) return null; ++ const canPick = lead.canPick === true && typeof pick2 === "function"; ++ const choose = async (handle) => { ++ setBusy(true); ++ setError(""); ++ try { ++ await pick2(handle); ++ setOpen(false); ++ } catch (err) { ++ setError(err?.message || t("Could not change the room lead.")); ++ } finally { ++ setBusy(false); + } -+ const ask = ++asked.current; -+ const timer = setTimeout(() => { -+ search(text2).then((list) => { -+ if (ask !== asked.current) return; -+ for (const app of list) names.current.set(app.slug, app.name); -+ setFound(list); -+ setFailed(null); -+ }, (failure) => { -+ if (ask !== asked.current) return; -+ setFound(null); -+ setFailed(failure?.message ?? String(failure)); -+ }); -+ }, 250); -+ return () => clearTimeout(timer); -+ }, [query, search]); -+ const nameOf = (slug) => names.current.get(slug) ?? appName(slug); -+ const picked = new Set(apps); -+ const needle = query.trim().toLowerCase(); -+ const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); -+ const results = (found ?? local).filter((app) => !picked.has(app.slug)); -+ return (0, import_react38.createElement)( ++ }; ++ return (0, import_react31.createElement)( + "div", -+ { className: "bt-connector-apps" }, -+ apps.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react38.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, -+ (0, import_react38.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), -+ failed ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, -+ (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react38.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { -+ names.current.set(app.slug, app.name); -+ onChange([...apps, app.slug]); -+ } }, app.name))) ++ { className: "bt-face-lead", "data-flybot": "face-lead" }, ++ (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Room lead")), ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-row" }, ++ (0, import_react31.createElement)( ++ "span", ++ { className: "bt-set-copy" }, ++ (0, import_react31.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), ++ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) ++ ), ++ canPick ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null ++ ), ++ open && canPick ? seats.map((seat) => (0, import_react31.createElement)( ++ "div", ++ { key: seat.key, className: "bt-set-row" }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), ++ seat === current ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) ++ )) : null, ++ open && canPick && lead.picked ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, ++ canPick || !lead.supported ? null : (0, import_react31.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), ++ error ? (0, import_react31.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null + ); +} -+function AppStatusList({ apps, statuses }) { -+ if (apps.length === 0 || Object.keys(statuses).length === 0) return null; -+ return (0, import_react38.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react38.createElement)( -+ "li", -+ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, -+ (0, import_react38.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), -+ (0, import_react38.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") -+ ))); ++function seatedCount(room2) { ++ const live = room2?.presentedParticipants ?? room2?.participants ?? []; ++ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length; ++ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length; ++ return humans + remotes + (room2?.members?.length ?? 0); +} -+function ConnectorRow({ connector, roster, actions }) { -+ const [form, setForm] = (0, import_react38.useState)(false); -+ const [bots, setBots] = (0, import_react38.useState)(false); -+ const [token, setToken] = (0, import_react38.useState)(""); -+ const [apps, setApps] = (0, import_react38.useState)(() => connector.apps ?? []); -+ const [picking, setPicking] = (0, import_react38.useState)(false); -+ const [busy, setBusy] = (0, import_react38.useState)(false); -+ const [error, setError] = (0, import_react38.useState)(null); -+ const [message, setMessage] = (0, import_react38.useState)(""); - const run = (task, after) => { - setBusy(true); - setError(null); -@@ -10184,83 +10184,195 @@ function ConnectorRow({ connector, actions }) { - after?.(); - }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); - }; -+ const gateway = connector.id === "composio"; -+ const sender = connector.kind === "sender"; -+ const channel = connector.kind === "channel"; -+ const modes = connector.modes ?? []; -+ const [mode, setMode] = (0, import_react38.useState)(() => connector.mode ?? modes[0]?.id); -+ const [values, setValues] = (0, import_react38.useState)({}); -+ const chosen = modes.find((entry) => entry.id === mode) ?? modes[0]; -+ const field = (name) => values[name] ?? ""; -+ const setField = (name, value) => setValues((current) => ({ ...current, [name]: value })); -+ const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== ""); -+ const [statuses, setStatuses] = (0, import_react38.useState)({}); -+ const appsKey = (connector.apps ?? []).join(","); -+ (0, import_react38.useEffect)(() => { -+ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { -+ setStatuses({}); -+ return void 0; -+ } -+ let live = true; -+ actions.connectorAppStatus(connector.id).then((value) => { -+ if (live) setStatuses(value ?? {}); -+ }, () => { -+ if (live) setStatuses({}); -+ }); -+ return () => { -+ live = false; -+ }; -+ }, [gateway, connector.state, appsKey, picking]); -+ const search = (0, import_react38.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); - const connect = () => { -- if (busy || token.trim() === "") return; -- void run(actions.connectorConnect(connector.id, token), () => { -+ if (busy) return; -+ if (channel) { -+ if (chosen === void 0 || !filled) return; -+ const credentials = Object.fromEntries(chosen.credentials.map((credential) => [credential.name, field(credential.name)])); -+ void run(actions.connectorConnect(connector.id, { mode: chosen.id, credentials }), () => { -+ setValues({}); -+ setForm(false); -+ }); -+ return; -+ } -+ if (token.trim() === "") return; -+ if (gateway && apps.length === 0) { -+ setError(t("Pick at least one app")); -+ return; -+ } -+ void run(actions.connectorConnect(connector.id, token, gateway ? apps : void 0), () => { - setToken(""); - setForm(false); - }); - }; -+ const saveApps = () => { -+ if (busy) return; -+ if (apps.length === 0) { -+ setError(t("Pick at least one app")); ++function roomSubtitle(room2, roster) { ++ if (room2.origin === "face-to-face") return faceRoomCount(room2, roster); ++ const admin = roster.byId[room2.admin]; ++ const count = seatedCount(room2); ++ return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); ++} ++function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { ++ const [adding, setAdding] = (0, import_react31.useState)(false); ++ const [confirming, setConfirming] = (0, import_react31.useState)(false); ++ (0, import_react31.useEffect)(() => { ++ if (!confirming) return void 0; ++ const timer = setTimeout(() => setConfirming(false), 4e3); ++ return () => clearTimeout(timer); ++ }, [confirming]); ++ const live = room2.presentedParticipants ?? room2.participants ?? []; ++ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); ++ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); ++ const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); ++ const face = room2.origin === "face-to-face"; ++ const people2 = face ? facePeople(room2, roster) : []; ++ const shared2 = humans.length > 1; ++ const chiefs = new Set((roster.humans ?? []).map((person) => person.designatedChiefId).filter(Boolean)); ++ const outside = roster.bots.filter((bot) => { ++ if (bot.remote) return false; ++ if (room2.members.includes(bot.id)) return false; ++ if (shared2) return chiefs.has(bot.id); ++ return true; ++ }); ++ const outsidePeople = (roster.humans ?? []).filter((person) => !humans.some((p) => p.id === person.id)); ++ const outsideRemotes = (roster.remoteBots ?? []).filter((bot) => !remotes.some((p) => p.handle === bot.handle)); ++ const admin = roster.byId[room2.admin]; ++ const mode = shownMode(room2, roster); ++ const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; ++ const update = (patch) => run(actions.updateRoom(room2.id, patch)); ++ const openBot = (id) => { ++ actions.closeSettings(); ++ actions.openSession(id); ++ }; ++ const remove = () => { ++ if (!confirming) { ++ setConfirming(true); + return; + } -+ void run(actions.connectorApps(connector.id, apps), () => setPicking(false)); ++ void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { ++ if (done) onDeleted?.(); ++ }); + }; - const on = connector.state !== "off"; -+ const canPost = sender || channel && connector.mode === "webhook"; -+ const perBot = !sender && (!channel || connector.mode === "app"); - const Mark = MARKS[connector.id]; -- const row = (0, import_react25.useRef)(null); -- useMorph(row, `${form}:${error !== null}:${connector.state}`); -- return (0, import_react25.createElement)( -+ const row = (0, import_react38.useRef)(null); -+ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`); -+ return (0, import_react38.createElement)( - "div", - { ref: row, className: "bt-connector", "data-state": connector.state }, -- (0, import_react25.createElement)( -+ (0, import_react38.createElement)( - "div", - { className: "bt-connector-head" }, -- (0, import_react25.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react25.createElement)(Mark) : null), -- (0, import_react25.createElement)( -+ (0, import_react38.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react38.createElement)(Mark) : null), -+ (0, import_react38.createElement)( - "span", - { className: "bt-connector-text" }, -- (0, import_react25.createElement)("span", { className: "bt-connector-name" }, connector.name), -- (0, import_react25.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) -+ (0, import_react38.createElement)("span", { className: "bt-connector-name" }, connector.name), -+ (0, import_react38.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) - ), -- (0, import_react25.createElement)( -+ (0, import_react38.createElement)( - "span", - { className: "bt-connector-actions" }, -- connector.state === "error" ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, -- on ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) -+ connector.state === "error" ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, -+ on && gateway && !picking ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ setApps(connector.apps ?? []); -+ setPicking(true); -+ } }, t("Choose apps")) : null, -+ on && perBot ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, -+ on ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) ++ return (0, import_react31.createElement)( ++ "div", ++ { className: "bt-bs" }, ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-bs-hero" }, ++ (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-bs-who" }, ++ (0, import_react31.createElement)("span", { className: "bt-bs-name" }, room2.name), ++ (0, import_react31.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) ++ ) ++ ), ++ (0, import_react31.createElement)( ++ Card, ++ { title: t("Profile") }, ++ (0, import_react31.createElement)( ++ Row, ++ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, ++ (0, import_react31.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) ++ ), ++ (0, import_react31.createElement)( ++ Row, ++ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, ++ (0, import_react31.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) ++ ) ++ ), ++ (0, import_react31.createElement)( ++ Card, ++ { title: t("Who answers") }, ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row bt-bs-stack" }, ++ (0, import_react31.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react31.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_react31.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) + ) + ), -+ on && gateway && !picking ? (0, import_react38.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, -+ on && perBot ? (0, import_react38.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, -+ on && bots ? (0, import_react38.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, -+ on && channel && connector.mode === "app" ? (0, import_react38.createElement)(AllowedSenders, { connector, actions, run, busy }) : null, -+ on && canPost ? (0, import_react38.createElement)( -+ "div", -+ { className: "bt-connector-form" }, -+ (0, import_react38.createElement)("label", { className: "bt-q-field" }, (0, import_react38.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })), -+ (0, import_react38.createElement)( ++ (0, import_react31.createElement)( ++ Card, ++ { title: t("Working folder") }, ++ (0, import_react31.createElement)( + "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_react38.createElement)("span", { className: "bt-connector-link" }), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send")) ++ { className: "bt-set-row bt-bs-row" }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), ++ (0, import_react31.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) + ) ++ ), ++ face ? (0, import_react31.createElement)( ++ Card, ++ { title: t("Members · {count}", { count: people2.length }) }, ++ people2.map((person) => (0, import_react31.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) + ) : null, -+ !on && form ? (0, import_react38.createElement)( -+ "div", -+ { className: "bt-connector-form" }, -+ channel ? (0, import_react38.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react38.createElement)("button", { -+ key: entry.id, -+ type: "button", -+ className: "bt-state", -+ "aria-pressed": entry.id === chosen?.id, -+ onClick: () => { -+ setMode(entry.id); -+ setValues({}); -+ setError(null); -+ } -+ }, t(entry.label)))) : null, -+ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react38.createElement)(CredentialField, { -+ key: credential.name, -+ credential, -+ value: field(credential.name), -+ onChange: (value) => setField(credential.name, value), -+ onEnter: connect, -+ autoFocus: index === 0 -+ })) : (0, import_react38.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react38.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), -+ gateway ? (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_react38.createElement)( -+ "a", -+ { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, -+ channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub") -+ ), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ setForm(false); -+ setToken(""); -+ setValues({}); -+ setError(null); -+ } }, t("Cancel")), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) - ) -- ), -- !on && form ? (0, import_react25.createElement)( -+ ) : null, -+ on && gateway && picking ? (0, import_react38.createElement)( - "div", - { className: "bt-connector-form" }, -- (0, import_react25.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react25.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: t("GitHub token"), autoFocus: true })), -- (0, import_react25.createElement)( -+ (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), -+ (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), -+ (0, import_react38.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -- setForm(false); -- setToken(""); -+ (0, import_react38.createElement)("span", { className: "bt-connector-link" }), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ setPicking(false); - setError(null); - } }, t("Cancel")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) - ) - ) : null, -- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null -+ error ? (0, import_react38.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null - ); - } - function ConnectorsPage({ roster, actions }) { - const list = roster?.connectors ?? []; -- return (0, import_react25.createElement)( -+ return (0, import_react38.createElement)( - "section", - { className: "bt-set-section" }, -- (0, import_react25.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react25.createElement)(ConnectorRow, { key: connector.id, connector, actions }))) -+ (0, import_react38.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react38.createElement)("div", { className: "bt-set-row" }, (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react38.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) - ); - } - - // src/client/feedback.js --var import_react26 = require("react"); --var FeedbackIcon = () => (0, import_react26.createElement)( -+var import_react39 = require("react"); -+var FeedbackIcon = () => (0, import_react39.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_react26.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), -- (0, import_react26.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) -+ (0, import_react39.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), -+ (0, import_react39.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) - ); --function download(name, text) { -- const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); -+function download(name, text2) { -+ const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); - const anchor = document.createElement("a"); - anchor.href = url; - anchor.download = name; - anchor.click(); - setTimeout(() => URL.revokeObjectURL(url), 1e3); - } --var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); --function whenText(time) { -+var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); -+function whenText2(time) { - 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) { - if (entry.source === "turn") return failureLabel(entry.code, entry.text); - return entry.text; - } --var SOURCES = { turn: "Model call", host: "DS Bot", api: "DS Bot", browser: "This window" }; --function FeedbackPage({ actions, run, Section, Row: Row3 }) { -- const [what, setWhat] = (0, import_react26.useState)(""); -- const [report, setReport] = (0, import_react26.useState)(null); -- const [preview, setPreview] = (0, import_react26.useState)(false); -- const [note, setNote] = (0, import_react26.useState)(""); -+var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" }; -+function FeedbackPage({ actions, run, Section, Row: Row4 }) { -+ const [what, setWhat] = (0, import_react39.useState)(""); -+ const [report, setReport] = (0, import_react39.useState)(null); -+ const [preview, setPreview] = (0, import_react39.useState)(false); -+ const [note, setNote] = (0, import_react39.useState)(""); - const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { - setReport(next); - return next; - }); - const load = () => fetchReport().then(() => void 0); -- (0, import_react26.useEffect)(() => { -+ (0, import_react39.useEffect)(() => { - void run(load()); - }, []); - const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +10289,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { - setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); - if (!copied) setPreview(true); - })); -- const [issueLink, setIssueLink] = (0, import_react26.useState)(null); -+ const [issueLink, setIssueLink] = (0, import_react39.useState)(null); - const openIssue = () => { - const win = window.open("", "_blank"); - if (win !== null) win.opener = null; -@@ -10307,8 +10307,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { - if (!inline && !copied) setPreview(true); - })); - }; -- const downloadLog = () => run(actions.diagnosticsLog().then((text) => { -- download("ds-bot-diagnostics.log", text || "No entries.\n"); -+ const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { -+ download("ds-bot-diagnostics.log", text2 || "No entries.\n"); - 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 total = (report?.total ?? 0) + browserErrors.length; - const shown = entries2.slice(0, 8); - return [ -- (0, import_react26.createElement)( -+ (0, import_react39.createElement)( - Section, - { key: "report", title: t("Report a problem") }, -- (0, import_react26.createElement)( -+ (0, import_react39.createElement)( ++ face ? null : (0, import_react31.createElement)( + Card, +- { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( ++ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, ++ humans.map((person) => (0, import_react31.createElement)( "div", - { className: "bt-set-row bt-set-paste bt-fb-what" }, -- (0, import_react26.createElement)( -+ (0, import_react39.createElement)( - "label", - { className: "bt-set-copy", htmlFor: "bt-fb-what" }, -- (0, import_react26.createElement)("span", null, t("What happened?")), -- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) -+ (0, import_react39.createElement)("span", null, t("What happened?")), -+ (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( ++ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, ++ (0, import_react31.createElement)( + "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ { className: "bt-gs-who" }, ++ (0, import_react31.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), ++ (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Human")), ++ person.role === "admin" ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Admin")) : null ++ ), ++ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null ++ )), ++ members.map((member) => { ++ const leads = room2.admin === member.id; ++ const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; ++ const taskLead = room2.taskLead === member.id; ++ return (0, import_react31.createElement)( ++ "div", ++ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, ++ (0, import_react31.createElement)( ++ "button", ++ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, ++ (0, import_react31.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), ++ (0, import_react31.createElement)("span", { className: "bt-row-title" }, member.name), ++ leads ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, ++ taskLead ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, ++ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Chief")) : null ++ ), ++ leads ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), ++ boardHere ? taskLead ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, ++ members.length > 2 ? (0, import_react31.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 ++ ); ++ }), ++ remotes.map((person) => (0, import_react31.createElement)( ++ "div", ++ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, ++ (0, import_react31.createElement)( ++ "span", ++ { className: "bt-gs-who" }, ++ (0, import_react31.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), ++ (0, import_react31.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), ++ // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is ++ // labelled as such; a Bot with no Bot Social identity can never be written to at all. ++ room2.origin === "face-to-face" ? null : (0, import_react31.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) ), -- (0, import_react26.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) -+ (0, import_react39.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) - ), -- (0, import_react26.createElement)( -- Row3, -+ (0, import_react39.createElement)( -+ Row4, - { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, -- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), -- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) - ), -- preview ? (0, import_react26.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react26.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, -- (0, import_react26.createElement)( -- Row3, -- { label: t("GitHub issue"), hint: t("Opens a new issue in the DS Bot repository with all of this filled in. Nothing is sent until you submit it there.") }, -- (0, import_react26.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) -+ preview ? (0, import_react39.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react39.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, -+ (0, import_react39.createElement)( -+ Row4, -+ { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) - ) - ), -- note ? (0, import_react26.createElement)( -+ note ? (0, import_react39.createElement)( - "div", - { key: "note", className: "bt-note bt-fb-note", role: "status" }, - note, -- issueLink ? [" ", (0, import_react26.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null -+ issueLink ? [" ", (0, import_react39.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null - ) : null, -- (0, import_react26.createElement)( -+ (0, import_react39.createElement)( - Section, - { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, -- shown.length === 0 ? (0, import_react26.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and DS Bot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react26.createElement)( -+ shown.length === 0 ? (0, import_react39.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react39.createElement)( - "div", - { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, -- (0, import_react26.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), -- (0, import_react26.createElement)( -+ (0, import_react39.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), -+ (0, import_react39.createElement)( - "div", +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) +- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) ++ // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. ++ room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") ++ )), ++ room2.origin === "face-to-face" ? null : adding ? [ ++ ...outsidePeople.map((person) => (0, import_react31.createElement)( ++ "button", ++ { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { ++ setAdding(false); ++ void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); ++ } }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), ++ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add person")) ++ )), ++ ...outside.map((bot) => (0, import_react31.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_react31.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, bot.name)), ++ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add")) ++ )), ++ ...outsideRemotes.map((bot) => (0, import_react31.createElement)( ++ "button", ++ { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { ++ setAdding(false); ++ void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); ++ } }, ++ (0, import_react31.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), ++ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) ++ )), ++ (0, import_react31.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) ++ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react31.createElement)(PlusIcon), t("Add member")) : null, ++ (0, import_react31.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react31.createElement)( ++ "span", ++ { className: "bt-set-hint" }, ++ // The host's two-Bot floor guards membership changes; this hint warns before one. ++ members.length + humans.length + remotes.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.")}${room2.origin === "face-to-face" || room2.origin === "inbound-share" ? "" : ` ${t("The task lead divides the board. When unset, the admin does.")}`}` ++ )), ++ // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after ++ // the note on who leads, so that note stays with the members it is about (#892). ++ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react31.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) + ), +- // The Main Bot tile is always first and always shown, like in the context menu. +- isTile ? null : (0, import_react18.createElement)( ++ (0, import_react31.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), ++ (0, import_react31.createElement)( + Card, + { title: t("Sidebar") }, +- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), +- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) ++ (0, import_react31.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_react31.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_react18.createElement)( ++ (0, import_react31.createElement)( + Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) +- ), +- isMainBot ? null : (0, import_react18.createElement)( ++ { title: t("More") }, ++ (0, import_react31.createElement)( + "button", + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react18.createElement)( ++ (0, import_react31.createElement)( + "span", { className: "bt-set-copy" }, -- (0, import_react26.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), -- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "DS Bot")].filter(Boolean).join(" · ")) -+ (0, import_react39.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), -+ (0, import_react39.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) ++ (0, import_react31.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), ++ (0, import_react31.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.")) ), -- entry.code ? (0, import_react26.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null -+ entry.code ? (0, import_react39.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null - )), -- (0, import_react26.createElement)( -- Row3, -+ (0, import_react39.createElement)( -+ Row4, - { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, -- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), -- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ (0, import_react31.createElement)("span", { className: "bt-chevron" }, (0, import_react31.createElement)(ChevronRightIcon)) ++ ) ++ ) ++ ); ++} ++function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { ++ const [admin, setAdmin] = (0, import_react31.useState)(null); ++ const [members, setMembers] = (0, import_react31.useState)([]); ++ const [name, setName] = (0, import_react31.useState)(""); ++ const [busy, setBusy] = (0, import_react31.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, ...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_react31.createElement)("button", { ++ key: bot.id, ++ type: "button", ++ role, ++ className: "bt-ng-chip", ++ "aria-checked": on, ++ onClick ++ }, (0, import_react31.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react31.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react31.createElement)(AdminStar) : null); ++ return (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-card bt-ng" }, ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row bt-bs-stack" }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react31.createElement)(AdminStar), t("Choose the group admin"))), ++ (0, import_react31.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")) ++ ) ++ ), ++ admin === null ? null : (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), ++ (0, import_react31.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")) ++ ) ++ ), ++ admin === null || members.length === 0 ? null : (0, import_react31.createElement)( ++ "label", ++ { className: "bt-set-row bt-bs-row bt-ng-step" }, ++ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Name"))), ++ (0, import_react31.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) ++ ), ++ (0, import_react31.createElement)( ++ "div", ++ { className: "bt-set-row bt-task-foot" }, ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ ) ++ ); ++} ++var GROUP_SETTINGS_CSS = ` ++.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} ++.bt-ng-step{animation:bt-veil-in .22s ease-out} ++.bt-ng-label{display:inline-flex;align-items:center;gap:6px} ++.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} ++.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} ++.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-ng-chip:active{scale:.96} ++.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} ++.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} ++.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} ++.bt-gs-member{gap:8px} ++.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} ++.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} ++.bt-gs-add{gap:8px;color:var(--bt-ink-2)} ++.bt-face-marks{display:inline-flex;flex:none;align-items:center;gap:2px} ++.bt-face-marks>.bt-mark{flex:none} ++.bt-face-person{cursor:default} ++.bt-face-person:hover .bt-row-title{text-decoration:none} ++.bt-face-person-text{display:flex;flex-direction:column;min-width:0} ++.bt-face-bots{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:16px;color:var(--bt-ink-3)} ++.bt-face-bot{padding:0;border:0;background:none;color:inherit;font:inherit} ++button.bt-face-bot{cursor:pointer} ++button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-underline-offset:2px} ++.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} ++.bt-bs-stack .bt-seg{align-self:stretch} ++@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} ++`; ++ ++// src/client/palette.js ++var import_react32 = require("react"); ++var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); ++var paletteText = (raw, names) => readableLine(String(raw ?? ""), names); ++var PALETTE_EXIT_MS = 160; ++function CommandPalette({ start, token, roster, actions, useSessions }) { ++ const [query, setQuery] = (0, import_react32.useState)(start ?? ""); ++ const [cursor, setCursor] = (0, import_react32.useState)(0); ++ const [leaving, setLeaving] = (0, import_react32.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 names = botNames(roster); ++ 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 text2 = paletteText(raw, names); ++ const at = text2.toLowerCase().indexOf(wanted); ++ if (at < 0) continue; ++ const from = Math.max(0, at - 24); ++ hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.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 people2 = [ ++ ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ ++ key: bot.id, ++ avatar: (0, import_react32.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), ++ title: pickerBotLabel(bot), ++ 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_react32.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), ++ title: room2.name, ++ subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), ++ badge: room2.hidden ? t("Hidden") : void 0, ++ run: async () => { ++ if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); ++ actions.openSession(room2.id); ++ } ++ })) ++ ]; ++ if (people2.length) sections.push([t("Bots"), people2]); ++ const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ ++ key: agent.id, ++ avatar: (0, import_react32.createElement)("span", { className: "bt-palette-agent" }, (0, import_react32.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_react32.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react32.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_react32.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, ++ { key: "cmd:bot", icon: (0, import_react32.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, ++ { key: "cmd:group", icon: (0, import_react32.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, ++ { key: "cmd:settings", icon: (0, import_react32.createElement)(GearIcon), title: t("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_react32.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(); ++ }; ++ let index = -1; ++ return (0, import_react32.createElement)( ++ "div", ++ { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, ++ (0, import_react32.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), ++ (0, import_react32.createElement)( ++ "div", ++ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, ++ (0, import_react32.createElement)( ++ "div", ++ { className: "bt-palette-head" }, ++ (0, import_react32.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react32.createElement)(SearchIcon)), ++ (0, import_react32.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_react32.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { ++ setQuery(""); ++ setCursor(0); ++ } }, (0, import_react32.createElement)(CloseIcon)) : null ++ ), ++ (0, import_react32.createElement)( ++ "div", ++ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, ++ (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ rows.length === 0 ? (0, import_react32.createElement)( ++ "div", ++ { className: "bt-palette-empty" }, ++ (0, import_react32.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react32.createElement)(SearchIcon)), ++ (0, import_react32.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), ++ (0, import_react32.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) ++ ) : sections.map(([title, items]) => (0, import_react32.createElement)( ++ import_react32.Fragment, ++ { key: title }, ++ (0, import_react32.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), ++ items.map((row) => { ++ index += 1; ++ const at = index; ++ return (0, import_react32.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_react32.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react32.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), ++ (0, import_react32.createElement)( ++ "span", ++ { className: "bt-palette-text" }, ++ (0, import_react32.createElement)("span", { className: "bt-palette-title" }, row.title), ++ row.subtitle ? (0, import_react32.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null ++ ), ++ row.badge ? (0, import_react32.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null ++ ); ++ }) ++ )) ) ) - ]; -@@ -10377,8 +10377,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { - var TOAST_MS = 6e3; - function ErrorToast({ useUi, actions }) { - const toast = useUi((state) => state.toast); -- const [leaving, setLeaving] = (0, import_react26.useState)(false); -- (0, import_react26.useEffect)(() => { -+ const [leaving, setLeaving] = (0, import_react39.useState)(false); -+ (0, import_react39.useEffect)(() => { - if (!toast) return void 0; - setLeaving(false); - const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) { - }; - }, [toast?.token]); - if (!toast) return null; -- return (0, import_react26.createElement)( -+ const info = toast.tone === "info"; -+ return (0, import_react39.createElement)( - "div", -- { className: "bt-toast", role: "alert", "data-leaving": leaving || void 0 }, -- (0, import_react26.createElement)("span", { className: "bt-toast-text" }, toast.text), -- (0, import_react26.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { -+ { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, -+ (0, import_react39.createElement)("span", { className: "bt-toast-text" }, toast.text), -+ info ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { - actions.dismissToast(toast.token); - actions.openSettings("feedback"); - } }, t("Details")), -- (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react26.createElement)(CloseIcon)) -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react39.createElement)(CloseIcon)) ); } - var FEEDBACK_CSS = ` -@@ -10416,47 +10416,56 @@ var FEEDBACK_CSS = ` - .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} - .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} - .bt-event-error{color:#e02135} --.bt-toast{position:fixed;left:50%;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100vw - 32px));padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;transform:translateX(-50%);animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} --.bt-toast[data-leaving]{opacity:0;transform:translate(-50%,6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} -+.bt-toast{position:fixed;left:0;right:0;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;width:fit-content;max-width:min(560px,calc(100vw - 32px));margin-inline:auto;padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} -+.bt-toast[data-leaving]{opacity:0;transform:translateY(6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} - .bt-toast-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical} - .bt-toast-text::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#e02135;margin:0 8px 1px 0} - .bt-toast-link{flex:none;border:0;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;padding:4px 6px;border-radius:6px} - .bt-toast-link:hover{background:var(--bt-hover);color:var(--bt-ink)} - .bt-toast-close{flex:none} --@keyframes bt-toast-in{from{transform:translate(-50%,16px) scale(.94)}} -+@keyframes bt-toast-in{from{transform:translateY(16px) scale(.94)}} - @media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}} - `; - - // src/client/about.js --var import_react27 = require("react"); --var CHEER_MS = 1800; -+var import_react40 = require("react"); - var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); - function botHint(update) { -- if (update.phase === "installing") return t("Updating DS Bot…"); -- if (update.phase === "installed") return t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }); -+ if (update.phase === "installing") return t("Updating FlyBot core…"); -+ if (update.phase === "installed") return t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }); - if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") }); -- if (update.available) return t("DS Bot {version} is available", { version: update.latest }); -+ if (update.available) return t("FlyBot core {version} is available", { version: update.latest }); - if (update.enabled === false) return t("Update checks are off."); - if (update.source === "path") return t("Installed from a local folder; update it there."); -+ if (update.source === "git" && update.build?.branch !== void 0) return branchHint(update); -+ if (update.source === "git" && update.build?.pinned) return t("Pinned to commit {commit}. Updates come from the maintainer: reinstall FlyBot to get them.", { commit: update.build.commit }); - if (!update.checkedAt) return null; - if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install."); -- const checked = t("Checked {time}", { time: whenText(update.checkedAt) }); -+ const checked = t("Checked {time}", { time: whenText2(update.checkedAt) }); - return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`; - } -+function branchHint({ build, behind, checkedAt }) { -+ const where = [build.branch, build.commit].filter(Boolean).join(" "); -+ if (build.commit === void 0 || !checkedAt) return t("Installed from GitHub ({where}). Updates come from the maintainer: reinstall FlyBot to get them.", { where }); -+ const checked = t("Checked {time}", { time: whenText2(checkedAt) }); -+ const state = behind === null ? t("Could not compare with {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : behind === 0 ? t("Up to date with {branch}.", { branch: build.branch }) : behind === 1 ? t("1 commit behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : t("{count} commits behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { count: behind, branch: build.branch }); -+ return `${state} · ${checked}`; -+} -+var versionLine = (update) => [t("Version {version}", { version: update.current }), [update.build?.branch, update.build?.commit].filter(Boolean).join(" ")].filter(Boolean).join(" · "); - function dshHint(dsh, shown) { - if (dsh.phase === "installing") return t("Updating DSH…"); - if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }); - if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") }); -- if (dsh.available === false && dsh.blockedBy) return t("Update DS Bot {version} first.", { version: dsh.blockedBy }); -+ if (dsh.available === false && dsh.blockedBy) return t("Update FlyBot core {version} first.", { version: dsh.blockedBy }); - if (shown) return t("DSH {version} is available", { version: dsh.verified }); - return t("Version {version}", { version: dsh.current }); - } - var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); --function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { -+function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) { - const update = useUpdate((value) => value) ?? {}; -- const [checking, setChecking] = (0, import_react27.useState)(false); -- const [stars, setStars] = (0, import_react27.useState)(null); -- (0, import_react27.useEffect)(() => { -+ const [checking, setChecking] = (0, import_react40.useState)(false); -+ const [stars, setStars] = (0, import_react40.useState)(null); -+ (0, import_react40.useEffect)(() => { - let live = true; - actions.repoStars().then((count) => { - if (live && Number.isInteger(count)) setStars(count); -@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { - live = false; - }; - }, []); -- const [cheer, setCheer] = (0, import_react27.useState)(true); -- (0, import_react27.useEffect)(() => { -- if (!cheer) return void 0; -- const timer = setTimeout(() => setCheer(false), CHEER_MS); -- return () => clearTimeout(timer); -- }, [cheer]); - const check = () => { - setChecking(true); -- void run(actions.checkUpdate()).then((done) => { -+ void run(actions.checkUpdate()).then(() => { - setChecking(false); -- if (done) setCheer(true); - }); - }; - const phase = update.phase ?? "idle"; - const dsh = update.dsh; - const dshPhase = dsh?.phase ?? "idle"; -- (0, import_react27.useEffect)(() => { -- if (phase === "installed" || dshPhase === "installed") setCheer(true); -- }, [phase, dshPhase]); -- const markState = checking ? "searching" : phase === "installing" || dshPhase === "installing" ? "working" : cheer ? "done" : "idle"; - const dshNew = showsDshRow(dsh, update.app); - const dshShown = dshNew && dsh.available; -- const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react27.createElement)( -+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react40.createElement)( - "button", - { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, - phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") -- ) : (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); -- const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react27.createElement)( -+ ) : (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); -+ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react40.createElement)( - "button", - { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, - dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") - ) : null; - const repo = update.repoUrl; - return [ -- (0, import_react27.createElement)( -+ (0, import_react40.createElement)( - "div", - { key: "hero", className: "bt-about-hero" }, -- (0, import_react27.createElement)("span", { className: "bt-about-mark" }, (0, import_react27.createElement)(BotMark, { look: MAIN_LOOK, bot: { id: "about" }, size: 52, state: markState, live: true, gaze: true, pokeable: true })), -- (0, import_react27.createElement)( -+ (0, import_react40.createElement)("span", { className: "bt-about-mark" }, (0, import_react40.createElement)(FlyBotMark, { size: 52 })), -+ (0, import_react40.createElement)( - "div", - { className: "bt-about-copy" }, -- (0, import_react27.createElement)("span", { className: "bt-about-name" }, "DS Bot"), -- update.current ? (0, import_react27.createElement)("span", { className: "bt-about-version" }, t("Version {version}", { version: update.current })) : null -+ (0, import_react40.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), -+ update.current ? (0, import_react40.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null - ), -- repo ? (0, import_react27.createElement)( -+ repo ? (0, import_react40.createElement)( - "a", - { - className: "bt-star", - href: repo, - target: "_blank", - rel: "noopener noreferrer", -- title: t("Star DS Bot on GitHub"), -- "aria-label": stars === null ? t("Star DS Bot on GitHub") : `${t("Star DS Bot on GitHub")} · ${t("{count} stars", { count: stars })}` -+ title: t("Star FlyBot on GitHub"), -+ "aria-label": stars === null ? t("Star FlyBot on GitHub") : `${t("Star FlyBot on GitHub")} · ${t("{count} stars", { count: stars })}` - }, -- (0, import_react27.createElement)("span", { className: "bt-star-main" }, (0, import_react27.createElement)("span", { className: "bt-star-icon" }, (0, import_react27.createElement)(StarLineIcon)), "Star"), -- stars === null ? null : (0, import_react27.createElement)("span", { className: "bt-star-count" }, starCount(stars)) -+ (0, import_react40.createElement)("span", { className: "bt-star-main" }, (0, import_react40.createElement)("span", { className: "bt-star-icon" }, (0, import_react40.createElement)(StarLineIcon)), "Star"), -+ stars === null ? null : (0, import_react40.createElement)("span", { className: "bt-star-count" }, starCount(stars)) - ) : null - ), -- repo ? (0, import_react27.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If DS Bot helps you, a star on GitHub helps more people find it.")) : null, -- (0, import_react27.createElement)( -+ repo ? (0, import_react40.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, -+ (0, import_react40.createElement)( - Section, - { key: "updates", title: t("Updates") }, -- (0, import_react27.createElement)(Row3, { label: "DS Bot", hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), -- dsh?.current ? (0, import_react27.createElement)(Row3, { label: "DeepSeek Harness", hint: dshHint(dsh, dshShown) }, dshControl) : null -+ (0, import_react40.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), -+ dsh?.current ? (0, import_react40.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null - ), -- (0, import_react27.createElement)( -+ (0, import_react40.createElement)( - Section, - { key: "links", title: t("Links") }, -- repo ? (0, import_react27.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react27.createElement)("a", linkProps(repo), (0, import_react27.createElement)(GitHubMark), t("Open"))) : null, -- update.notesUrl ? (0, import_react27.createElement)(Row3, { label: t("Release notes") }, (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, -- (0, import_react27.createElement)(Row3, { label: t("Report a problem") }, (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) -+ repo ? (0, import_react40.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react40.createElement)("a", linkProps(repo), (0, import_react40.createElement)(GitHubMark), t("Open"))) : null, -+ update.notesUrl ? (0, import_react40.createElement)(Row4, { label: t("Release notes") }, (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, -+ (0, import_react40.createElement)(Row4, { label: t("Report a problem") }, (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) - ) - ]; - } - var ABOUT_CSS = ` - .bt-about-hero{display:flex;align-items:center;gap:14px;padding:16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} --.bt-about-mark{flex:none;display:inline-flex;position:relative} --.bt-about-mark::before{content:'';position:absolute;inset:-20%;border-radius:50%} --body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle]:not(.bt-mark-poke) .bt-b{rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} --body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye{scale:1 0;animation:none} --body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye-happy{opacity:1;transition-delay:.1s} -+.bt-about-mark{flex:none;display:inline-flex} - .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} - .bt-about-name{font-size:16px;line-height:22px;font-weight:600} - .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s - `; +-var BOT_SETTINGS_CSS = ` +-.bt-bs{display:flex;flex-direction:column;gap:18px} +-.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} +-.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} +-.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-bs .bt-avedit{left:0;transform:none} +-.bt-bs-row{min-height:48px} +-label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} +-.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +-.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +-.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} +-.bt-bs-input:hover{background:var(--bt-hover)} +-.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +-.bt-bs-input::placeholder{color:var(--bt-ink-3)} +-.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} +-.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} +-.bt-bs-area:hover{background:var(--bt-active)} +-.bt-bs-danger .bt-bs-label{color:#c21d2e} +-.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} +-.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} +-.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} +-.bt-switch[aria-checked=true]{background:var(--bt-accent)} +-.bt-switch:disabled{opacity:.45;cursor:default} +-.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} +-.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} +-.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} +-.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} +-.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} +-.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} +-@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +-`; - // src/client/settings.js --var import_react28 = require("react"); -+var import_react41 = require("react"); - var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); --var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About DS Bot"]]; --var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; -+var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["projects", "Projects"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["workflows", "Workflows"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; -+var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; - function SetSection({ title, children }) { -- return (0, import_react28.createElement)("section", { className: "bt-set-section" }, (0, import_react28.createElement)("h3", null, title), (0, import_react28.createElement)("div", { className: "bt-set-card" }, children)); -+ return (0, import_react41.createElement)("section", { className: "bt-set-section" }, (0, import_react41.createElement)("h3", null, title), (0, import_react41.createElement)("div", { className: "bt-set-card" }, children)); + // src/client/bot-draft.js + function freshBotName(roster, base = "New Bot") { +@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { } - function SetRow({ label, hint, children }) { -- return (0, import_react28.createElement)( -+ return (0, import_react41.createElement)( - "div", - { className: "bt-set-row" }, -- (0, import_react28.createElement)("div", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", null, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- children ? (0, import_react28.createElement)("div", { className: "bt-set-control" }, children) : null -+ (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, label), hint ? (0, import_react41.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ children ? (0, import_react41.createElement)("div", { className: "bt-set-control" }, children) : null - ); + function startModel(roster) { + const models = roster.models ?? []; +- return (models.find((model) => model.ref === roster.defaultModel) ?? models[0])?.ref ?? ""; ++ const served = (ref) => ref ? models.find((model) => model.ref === ref) : void 0; ++ const main = roster.byId?.[roster.mainBotId] ?? (roster.bots ?? []).find((bot) => bot.id === roster.mainBotId); ++ return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? ""; } -+var NOTIFY_KINDS = [ -+ ["approval", "Waiting for approval", "Once more if it still waits after 15 min."], -+ ["finished", "Finished"], -+ ["failed", "Failed"] -+]; - function GeneralPage({ roster, actions, run }) { - const prefs = roster.prefs ?? {}; - const themes = allThemes(); - const current = themes[prefs.theme] ? prefs.theme : "deepseek"; -- const [custom, setCustom] = (0, import_react28.useState)(prefs.accent ?? "#4d6bfe"); -- const timer = (0, import_react28.useRef)(0); -- (0, import_react28.useEffect)(() => () => clearTimeout(timer.current), []); -+ const [custom, setCustom] = (0, import_react41.useState)(prefs.accent ?? "#4d6bfe"); -+ const timer = (0, import_react41.useRef)(0); -+ (0, import_react41.useEffect)(() => () => clearTimeout(timer.current), []); - const pickCustom = (value) => { - setCustom(value); - clearTimeout(timer.current); - timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350); +-function createPayload({ name, look, image, model, effort, brief }) { ++function createPayload({ name, look, image, model, effort }) { + const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } }; + if (model) payload.model = model; + if (effort) payload.effort = effort; +- if (brief) payload.brief = brief; + return payload; + } ++async function afterCreate({ id, text: text2, actions }) { ++ actions.openSession(id); ++ actions.showDetails(id, "settings"); ++ const line2 = String(text2 ?? "").trim(); ++ if (line2 === "") return; ++ try { ++ await actions.send(id, line2); ++ } catch (failure) { ++ actions.notify?.(failure?.message ?? String(failure)); ++ } ++} + + // src/client/create-bot.js +-var import_react19 = require("react"); ++var import_react33 = require("react"); + var pick = (list) => list[Math.floor(Math.random() * list.length)]; + var randomLook = (avoid = {}) => { + const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); + const colors = DEFAULT_COLORS.filter((color) => color !== avoid.color); + return { shape: pick(shapes), color: pick(colors) }; + }; +-function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCreated, inline = false }) { +- const [look, setLook] = (0, import_react19.useState)(() => randomLook()); +- const [image, setImage] = (0, import_react19.useState)(null); +- const [name, setName] = (0, import_react19.useState)(start?.name ?? ""); +- const [model, setModel] = (0, import_react19.useState)(() => startModel(roster)); +- const [effort, setEffort] = (0, import_react19.useState)(void 0); +- const [busy, setBusy] = (0, import_react19.useState)(false); +- const [error, setError] = (0, import_react19.useState)(""); +- const fileRef = (0, import_react19.useRef)(null); +- const frame = (0, import_react19.useRef)(null); ++function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { ++ const [look, setLook] = (0, import_react33.useState)(() => randomLook()); ++ const [image, setImage] = (0, import_react33.useState)(null); ++ const [name, setName] = (0, import_react33.useState)(start?.name ?? ""); ++ const [model, setModel] = (0, import_react33.useState)(() => startModel(roster)); ++ const [effort, setEffort] = (0, import_react33.useState)(void 0); ++ const [busy, setBusy] = (0, import_react33.useState)(false); ++ const [error, setError] = (0, import_react33.useState)(""); ++ const fileRef = (0, import_react33.useRef)(null); ++ const frame = (0, import_react33.useRef)(null); + useEscape(onBack); +- (0, import_react19.useEffect)(() => { ++ (0, import_react33.useEffect)(() => { + if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); + }, []); + const fallbackName = freshBotName(roster, t("New Bot")); +@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr + setError(""); + setBusy(true); + try { +- const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief })); +- onCreated(bot); ++ const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort })); ++ await onCreated(bot); + } catch (failure) { + setError(failure?.message ?? String(failure)); + setBusy(false); +@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr + avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; - return [ -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetSection, - { key: "look", title: t("Appearance") }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetRow, - { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "select", - { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, -- Object.entries(themes).map(([id, theme]) => (0, import_react28.createElement)("option", { key: id, value: id }, t(theme.label))) -+ Object.entries(themes).map(([id, theme]) => (0, import_react41.createElement)("option", { key: id, value: id }, t(theme.label))) - ) + const previewLook = image ? { ...look, image } : look; +- const card = (0, import_react19.createElement)( ++ const card = (0, import_react33.createElement)( + "div", + { className: "bt-create-card" }, +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( + "div", + { className: "bt-create-preview" }, + // The key remounts the mark, so every new pick plays the pop. +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( + "span", + { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, +- (0, import_react19.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) ++ (0, import_react33.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) ), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetRow, - { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "div", - { className: "bt-swatches" }, -- (0, import_react28.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), -- ACCENT_INKS.map((ink) => (0, import_react28.createElement)("button", { -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), -+ ACCENT_INKS.map((ink) => (0, import_react41.createElement)("button", { - key: ink, - type: "button", - className: "bt-swatch", -@@ -10626,75 +10626,86 @@ function GeneralPage({ roster, actions, run }) { - style: { background: INKS[ink][0] }, - onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) - })), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "label", - { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, -- (0, import_react28.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) -+ (0, import_react41.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) - ) - ) - ) - ), -- (0, import_react28.createElement)( -+ // One switch for every system notification (#353), then one per kind (#469). -+ (0, import_react41.createElement)( - SetSection, -- { key: "mode", title: t("Mode") }, -- (0, import_react28.createElement)( -- SetRow, -- { label: t("Classic Agent mode") }, -- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch")) -- ) -+ { key: "notify", title: t("Notifications") }, -+ (0, import_react41.createElement)(SwitchRow, { -+ label: t("In other chats"), -+ hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), -+ on: prefs.notifications !== false, -+ onChange: (on) => run(actions.setPrefs({ notifications: on })) -+ }), -+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react41.createElement)(SwitchRow, { -+ key: kind, -+ label: t(label), -+ hint: hint ? t(hint) : void 0, -+ disabled: prefs.notifications === false, -+ on: prefs.notify?.[kind] !== false, -+ onChange: (on) => run(actions.setPrefs({ notify: { [kind]: on } })) -+ })) - ) - ]; - } - function MainBotsSection({ roster, actions, run }) { - const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean); - const [first, ...others] = mains; -- const candidates = roster.bots.filter((bot) => !isMainOf(roster, bot.id)); -- return (0, import_react28.createElement)( -+ const own = roster.bots.filter((bot) => !bot.remote); -+ const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); -+ return (0, import_react41.createElement)( - SetSection, - { title: t("Main Bot") }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetRow, - { - label: t("Main Bot"), - hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 - }, -- (0, import_react28.createElement)("select", { -+ (0, import_react41.createElement)("select", { - className: "bt-select", - "aria-label": t("Main Bot"), - value: first?.id ?? "", - onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) -- }, first ? null : (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), roster.bots.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ }, first ? null : (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) +- (0, import_react19.createElement)("span", { className: "bt-create-name" }, finalName) ++ (0, import_react33.createElement)("span", { className: "bt-create-name" }, finalName) ), -- others.map((bot) => (0, import_react28.createElement)( -+ others.map((bot) => (0, import_react41.createElement)( - SetRow, - { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, -- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), -- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) - )), -- candidates.length > 0 ? (0, import_react28.createElement)( -+ candidates.length > 0 ? (0, import_react41.createElement)( - SetRow, - { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, -- (0, import_react28.createElement)("select", { -+ (0, import_react41.createElement)("select", { - className: "bt-select", - "aria-label": t("Add a Main Bot"), - value: "", - onChange: (event) => { - if (event.target.value) void run(actions.setMain(event.target.value, true)); - } -- }, (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ }, (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) - ) : null - ); - } - var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]]; - function MotionSection({ roster, actions, run }) { - const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; -- return (0, import_react28.createElement)( -+ return (0, import_react41.createElement)( - SetSection, - { title: t("Animation") }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetRow, - { label: t("Bot animation") }, -- (0, import_react28.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react28.createElement)("button", { -+ (0, import_react41.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react41.createElement)("button", { - key: id, - type: "button", - "aria-pressed": motion === id, -@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) { - ); - } - function BotsPage({ roster, actions, run }) { -- const [creating, setCreating] = (0, import_react28.useState)(false); -+ const [creating, setCreating] = (0, import_react41.useState)(false); - return [ -- (0, import_react28.createElement)(MainBotsSection, { key: "main", roster, actions, run }), -- (0, import_react28.createElement)(MotionSection, { key: "motion", roster, actions, run }), -- creating ? (0, import_react28.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react28.createElement)( -+ (0, import_react41.createElement)(MainBotsSection, { key: "main", roster, actions, run }), -+ (0, import_react41.createElement)(MotionSection, { key: "motion", roster, actions, run }), -+ creating ? (0, import_react41.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react41.createElement)( +- (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")), +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)("div", { className: "bt-create-step" }, t("Look")), ++ (0, import_react33.createElement)( "div", - { key: "new", className: "bt-actions" }, -- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("New Bot")) -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("New Bot")) + { className: "bt-create-looks" }, +- (0, import_react19.createElement)(CharacterGrid, { ++ (0, import_react33.createElement)(CharacterGrid, { + look, + hasPhoto: Boolean(image), + onShape: (shape) => { +@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr + setLook((current) => ({ ...current, color })); + } + }), +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( + "div", + { className: "bt-actions bt-create-lookbar" }, +- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { ++ (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { + setImage(null); + setLook((current) => randomLook(current)); + } }, t("Shuffle")), +- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), +- image ? (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, +- (0, import_react19.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { ++ (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), ++ image ? (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, ++ (0, import_react33.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { + const file = event.target.files?.[0]; + event.target.value = ""; + upload(file); + } }) + ) ), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetSection, -- { key: "bots", title: t("Bots · {count}", { count: roster.bots.length }) }, -- roster.bots.map((bot) => (0, import_react28.createElement)( -+ { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, -+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react41.createElement)( - "button", - { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "span", - { className: "bt-set-bot" }, -- (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), -- (0, import_react28.createElement)("span", { className: "bt-row-title" }, bot.name), -- bot.role ? (0, import_react28.createElement)("span", { className: "bt-tag" }, bot.role) : null, -- isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, -- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ (0, import_react41.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), -+ (0, import_react41.createElement)("span", { className: "bt-row-title" }, bot.name), -+ bot.role ? (0, import_react41.createElement)("span", { className: "bt-tag" }, bot.role) : null, -+ isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, -+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null - ), -- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) -+ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon)) - )) - ) - ]; - } - function GroupsPage({ roster, actions, run }) { -- const [creating, setCreating] = (0, import_react28.useState)(false); -+ const [creating, setCreating] = (0, import_react41.useState)(false); - return [ -- creating ? (0, import_react28.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react28.createElement)( -+ creating ? (0, import_react41.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react41.createElement)( +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( + "label", + { className: "bt-field" }, +- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")), +- (0, import_react19.createElement)("input", { ++ (0, import_react33.createElement)("span", { className: "bt-field-label" }, t("Name")), ++ (0, import_react33.createElement)("input", { + className: "bt-input", + autoFocus: !inline, + value: name, +@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr + } + } + }), +- problem ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null ++ problem ? (0, import_react33.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react33.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null + ), +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( "div", - { key: "new", className: "bt-actions" }, -- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("Create group chat")) -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("Create group chat")) + { className: "bt-field" }, +- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Model")), +- (0, import_react19.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { ++ (0, import_react33.createElement)("span", { className: "bt-field-label" }, t("Model")), ++ (0, import_react33.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { + setModel(ref); + setEffort(void 0); + }, onPickEffort: setEffort }), +- (0, import_react19.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) ++ (0, import_react33.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) ), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - SetSection, - { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, -- roster.rooms.length === 0 ? (0, import_react28.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { -+ roster.rooms.length === 0 ? (0, import_react41.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { - const admin = roster.byId[room2.admin]; -- return (0, import_react28.createElement)( -+ return (0, import_react41.createElement)( - "button", - { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "span", - { className: "bt-set-bot" }, -- (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react28.createElement)("span", { className: "bt-row-title" }, room2.name), -- (0, import_react28.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })), -- admin ? (0, import_react28.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react28.createElement)(AdminStar), admin.name) : null, -- room2.hidden ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -+ (0, import_react41.createElement)("span", { className: "bt-row-title" }, room2.name), -+ (0, import_react41.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), -+ admin ? (0, import_react41.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react41.createElement)(AdminStar), admin.name) : null, -+ room2.hidden ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null - ), -- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) -+ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon)) - ); - }) +- error ? (0, import_react19.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, +- (0, import_react19.createElement)( ++ error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, ++ (0, import_react33.createElement)( + "div", + { className: "bt-create-foot" }, +- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")), +- (0, import_react19.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) ++ // The dialog's header already has Back and Close (#230); only the inline form needs its own. ++ inline ? (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, ++ (0, import_react33.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) ) - ]; - } - function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { -- const [error, setError] = (0, import_react28.useState)(""); -- const close = (0, import_react28.useCallback)(() => actions.closeSettings(), []); -+ const [error, setError] = (0, import_react41.useState)(""); -+ const close = (0, import_react41.useCallback)(() => actions.closeSettings(), []); - useEscape(close); - const run = (promise) => Promise.resolve(promise).then(() => { - setError(""); -@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false - }); - const bot = page === "bots" && id ? roster.byId[id] : void 0; - const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; -- const shown = SETTINGS_PAGES.some(([key]) => key === page) ? page : "general"; -+ const extra = useSettingsPages().map((entry) => ({ entry, key: `plugin:${entry.id}` })); -+ const pages = SETTINGS_PAGES.flatMap((item) => item[0] === "connectors" ? [item, ...extra.map(({ entry, key }) => [key, entry.label ?? entry.id])] : [item]); -+ const shown = pages.some(([key]) => key === page) ? page : "general"; -+ const pluginPage = extra.find(({ key }) => key === shown)?.entry; - const usageFocus = shown === "usage" && id ? id : null; -- const label = t(SETTINGS_PAGES.find(([key]) => key === shown)[1]); -+ const label = t(pages.find(([key]) => key === shown)[1]); - const back = bot ? t("Back to Bots") : room2 ? t("Back to group chats") : usageFocus ? t("All usage") : null; - const nav = useGlide(shown, { axis: "y" }); - const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; - const depth = bot || room2 ? 1 : 0; -- const last = (0, import_react28.useRef)({ key: pageKey, depth, move: "up" }); -+ const last = (0, import_react41.useRef)({ key: pageKey, depth, move: "up" }); - if (last.current.key !== pageKey) { - last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; - } -- const [usageOpened, setUsageOpened] = (0, import_react28.useState)(shown === "usage"); -+ const [usageOpened, setUsageOpened] = (0, import_react41.useState)(shown === "usage"); - if (shown === "usage" && !usageOpened) setUsageOpened(true); -- const errorLine = error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -- return (0, import_react28.createElement)( -+ const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -+ return (0, import_react41.createElement)( + ); +- if (inline) return (0, import_react19.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); +- return (0, import_react19.createElement)( ++ if (inline) return (0, import_react33.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); ++ return (0, import_react33.createElement)( "div", - { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, -- (0, import_react28.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react41.createElement)( + { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, +- (0, import_react19.createElement)( ++ (0, import_react33.createElement)( "div", -- { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("DS Bot settings"), "data-wide": shown === "usage" ? "" : void 0 }, -- (0, import_react28.createElement)( -+ { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, -+ (0, import_react41.createElement)( - "nav", - { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, -- (0, import_react28.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), -- SETTINGS_PAGES.map(([key, text]) => (0, import_react28.createElement)("button", { -+ (0, import_react41.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), -+ pages.map(([key, text2]) => (0, import_react41.createElement)("button", { - key, - type: "button", - className: "bt-settings-item", - "aria-current": shown === key ? "page" : void 0, - onClick: () => actions.openSettings(key) -- }, (0, import_react28.createElement)(PAGE_ICONS[key]), t(text))), -- (0, import_react28.createElement)("button", { -+ }, (0, import_react41.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))), -+ // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot -+ // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). -+ (0, import_react41.createElement)("button", { - type: "button", - className: "bt-settings-item bt-settings-shell", -- title: t("Models, providers, and the rest of DeepSeek Harness"), -+ title: t("Opens DeepSeek Harness's own settings (models, providers, permissions) in a separate window"), -+ "aria-label": t("Harness settings, opens DeepSeek Harness's own settings window"), - onClick: () => { - close(); - openHarnessSettings(); - } -- }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings")) -+ }, (0, import_react41.createElement)(SlidersIcon), t("Harness settings"), (0, import_react41.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) - ), -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "div", - { className: "bt-settings-main" }, -- (0, import_react28.createElement)( -+ (0, import_react41.createElement)( - "div", - { className: "bt-settings-head" }, -- back ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react28.createElement)(ChevronLeftIcon)) : null, -- (0, import_react28.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) -+ back ? (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react41.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react41.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) - ), -- shown === "usage" ? null : (0, import_react28.createElement)( -+ shown === "usage" ? null : (0, import_react41.createElement)( - "div", - { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, - errorLine, -- shown === "general" ? (0, import_react28.createElement)(GeneralPage, { roster, actions, run }) : null, -- bot ? (0, import_react28.createElement)("div", { className: "bt-settings-form" }, (0, import_react28.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, -- shown === "bots" && !bot ? (0, import_react28.createElement)(BotsPage, { roster, actions, run }) : null, -- room2 ? (0, import_react28.createElement)("div", { className: "bt-settings-form" }, (0, import_react28.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, -- shown === "groups" && !room2 ? (0, import_react28.createElement)(GroupsPage, { roster, actions, run }) : null, -- shown === "connectors" ? (0, import_react28.createElement)(ConnectorsPage, { roster, actions }) : null, -- shown === "capabilities" ? (0, import_react28.createElement)(CapabilitiesPage, { roster, actions, run }) : null, -- shown === "schedules" ? (0, import_react28.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, -- shown === "secrets" ? (0, import_react28.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, -- shown === "feedback" ? (0, import_react28.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, -- shown === "about" ? (0, import_react28.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null -+ shown === "general" ? (0, import_react41.createElement)(GeneralPage, { roster, actions, run }) : null, -+ bot ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, -+ shown === "bots" && !bot ? (0, import_react41.createElement)(BotsPage, { roster, actions, run }) : null, -+ room2 ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, -+ shown === "groups" && !room2 ? (0, import_react41.createElement)(GroupsPage, { roster, actions, run }) : null, -+ shown === "projects" ? (0, import_react41.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null, -+ shown === "connectors" ? (0, import_react41.createElement)(ConnectorsPage, { roster, actions }) : null, -+ shown === "workflows" ? (0, import_react41.createElement)(WorkflowsPage, { call: call2, roster }) : null, -+ pluginPage ? (0, import_react41.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, -+ shown === "capabilities" ? (0, import_react41.createElement)(CapabilitiesPage, { roster, actions, run }) : null, -+ shown === "schedules" ? (0, import_react41.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, -+ shown === "secrets" ? (0, import_react41.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, -+ shown === "feedback" ? (0, import_react41.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, -+ shown === "about" ? (0, import_react41.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null - ), -- usageOpened ? (0, import_react28.createElement)( -+ usageOpened ? (0, import_react41.createElement)( - "div", - { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, - shown === "usage" ? errorLine : null, -- (0, import_react28.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) -+ (0, import_react41.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) - ) : null - ), -- (0, import_react28.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react28.createElement)(CloseIcon)) -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) - ) + { className: "bt-create-head" }, +- canGoBack ? (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react19.createElement)(ChevronLeftIcon)) : (0, import_react19.createElement)("span", { className: "bt-create-gap" }), +- (0, import_react19.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), +- (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react19.createElement)(CloseIcon)) ++ canGoBack ? (0, import_react33.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react33.createElement)(ChevronLeftIcon)) : (0, import_react33.createElement)("span", { className: "bt-create-gap" }), ++ (0, import_react33.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), ++ (0, import_react33.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react33.createElement)(CloseIcon)) + ), +- (0, import_react19.createElement)("div", { className: "bt-create-body" }, card) ++ (0, import_react33.createElement)("div", { className: "bt-create-body" }, card) ); } -+var openProjectBoard = (id) => { -+ if (typeof window !== "undefined") window.location.hash = `#project=${id}`; -+}; -+var projectMembers = (roster) => [ -+ ...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })), -+ ...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null })) -+]; -+function ProjectsList({ projects, roster, call: call4, reload }) { -+ const members = projectMembers(roster); -+ const bind = async (choice, project) => { -+ const [kind, id] = String(choice ?? "").split(":"); -+ if (!kind || !id) return; -+ await call4("project-bind", { kind, id, projectId: project.id }); -+ await reload?.(); -+ }; -+ const unbind = async (member) => { -+ await call4("project-bind", { kind: member.kind, id: member.id, projectId: null }); -+ await reload?.(); + + // src/client/new-chat.js +-var import_react20 = require("react"); +-var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives"); ++var import_react34 = require("react"); + function NewChat({ mode, roster, actions }) { +- const [query, setQuery] = (0, import_react20.useState)(""); +- const [picked, setPicked] = (0, import_react20.useState)([]); +- const [group, setGroup] = (0, import_react20.useState)(mode === "group"); +- const [admin, setAdmin] = (0, import_react20.useState)(null); +- const [message, setMessage] = (0, import_react20.useState)(""); +- const [busy, setBusy] = (0, import_react20.useState)(false); +- const [error, setError] = (0, import_react20.useState)(""); +- const [cursor, setCursor] = (0, import_react20.useState)(0); +- const [draft, setDraft] = (0, import_react20.useState)(mode === "create" ? { name: "", fromList: false } : null); +- const inputRef = (0, import_react20.useRef)(null); ++ const [query, setQuery] = (0, import_react34.useState)(""); ++ const [picked, setPicked] = (0, import_react34.useState)([]); ++ const [group, setGroup] = (0, import_react34.useState)(mode === "group"); ++ const [compose, setCompose] = (0, import_react34.useState)(mode !== "new"); ++ const [admin, setAdmin] = (0, import_react34.useState)(null); ++ const [message, setMessage] = (0, import_react34.useState)(""); ++ const [busy, setBusy] = (0, import_react34.useState)(false); ++ const [error, setError] = (0, import_react34.useState)(""); ++ const [fix, setFix] = (0, import_react34.useState)(null); ++ const [fixNote, setFixNote] = (0, import_react34.useState)(""); ++ const [inviteLink, setInviteLink] = (0, import_react34.useState)(""); ++ const [cursor, setCursor] = (0, import_react34.useState)(0); ++ const [draft, setDraft] = (0, import_react34.useState)(mode === "create" ? { name: "", fromList: false } : null); ++ const inputRef = (0, import_react34.useRef)(null); + const close = () => actions.closeOverlay(); + useEscape(close); + const paneLeft = usePaneLeft(); + const wanted = query.trim().toLowerCase(); +- const matches = roster.bots.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); ++ const bots = listedChatBots(roster); + const choosingAdmin = group && admin === null; +- const exact = roster.bots.some((bot) => bot.name.toLowerCase() === wanted); ++ const pool = choosingAdmin ? bots.filter((bot) => !bot.remote) : bots; ++ const matches = pool.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || pickerBotLabel(bot).toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); ++ const exact = bots.some((bot) => bot.name.toLowerCase() === wanted); + const pickedBots = picked.map((id) => roster.byId[id]).filter(Boolean); + const run = async (task) => { + if (busy) return; + setError(""); ++ setFix(null); ++ setFixNote(""); ++ setInviteLink(""); + setBusy(true); + try { + await task(); + } catch (failure) { + setError(failure?.message ?? String(failure)); ++ setFix(ungrantedLocalFix(failure?.details) ?? null); + } finally { + setBusy(false); + } + }; ++ const leaveOut = (id) => { ++ if (id === admin) setAdmin(null); ++ setPicked((current) => current.filter((other) => other !== id)); ++ setError(""); ++ setFix(null); ++ setFixNote(""); ++ setInviteLink(""); + }; -+ const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0); -+ return [ -+ (0, import_react41.createElement)( -+ SetSection, -+ { key: "projects", title: t("Projects · {count}", { count: projects.length }) }, -+ projects.length === 0 ? (0, import_react41.createElement)("div", { className: "bt-set-row" }, (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react41.createElement)( -+ SetRow, -+ { -+ key: project.id, -+ label: project.name, -+ hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir -+ }, -+ (0, import_react41.createElement)( -+ "div", -+ { className: "bt-project-row-controls" }, -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")), -+ (0, import_react41.createElement)( -+ "select", -+ { -+ className: "bt-select", -+ "aria-label": t("Bind to {name}", { name: project.name }), -+ value: "", -+ onChange: (event) => { -+ void bind(event.target.value, project); -+ } -+ }, -+ (0, import_react41.createElement)("option", { value: "" }, t("Bind a Bot or group")), -+ members.map((member) => (0, import_react41.createElement)( -+ "option", -+ { key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` }, -+ `${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}` -+ )) -+ ) -+ ) -+ )) -+ ), -+ bound2.length === 0 ? null : (0, import_react41.createElement)( -+ SetSection, -+ { key: "bindings", title: t("Bindings") }, -+ bound2.map((member) => (0, import_react41.createElement)( -+ SetRow, -+ { -+ key: `${member.kind}:${member.id}`, -+ label: member.name, -+ hint: member.kind === "group" ? t("Group") : t("Bot") -+ }, -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ void unbind(member); -+ } }, t("Unbind")) -+ )) -+ ) -+ ]; -+} -+function ProjectsPage({ roster, actions, run, call: call4 }) { -+ const [state, setState] = (0, import_react41.useState)({ projects: [], error: null }); -+ const [name, setName] = (0, import_react41.useState)(""); -+ const [workDir, setWorkDir] = (0, import_react41.useState)(null); -+ const reload = (0, import_react41.useCallback)(async () => { -+ const listing = await call4("projects"); -+ setState({ projects: listing?.projects ?? [], error: null }); -+ }, [call4]); -+ (0, import_react41.useEffect)(() => { -+ void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) })); -+ }, [reload]); -+ const create = async () => { -+ const trimmed3 = name.trim(); -+ if (trimmed3 === "") return; -+ const body = { name: trimmed3 }; -+ if (workDir !== null && workDir !== "") body.workDir = workDir; ++ const copyInvite = async (bot) => { ++ const social = globalThis.__flybot__?.social; ++ let link; + try { -+ await call4("project-create", body); -+ setName(""); -+ setWorkDir(null); -+ await reload(); -+ } catch (error) { -+ setState({ projects: state.projects, error: error?.message ?? String(error) }); ++ const invite = await social.createInvite(bot.id); ++ link = invite?.link || invite?.url; ++ if (!link) throw new Error("no link"); ++ } catch { ++ setInviteLink(""); ++ setFixNote(t("Could not create the invite. Use Bot settings → Sharing → Copy invite link for this Bot.")); ++ return; ++ } ++ if (await copiedToClipboard(link)) { ++ setInviteLink(""); ++ setFixNote(t("Invite link for {name} copied. Send it to your friend; once they accept, create the group again.", { name: bot.name })); ++ } else { ++ setInviteLink(link); ++ setFixNote(t("Could not copy the invite link for {name}. Copy the link below and send it to your friend; once they accept, create the group again.", { name: bot.name })); + } + }; -+ return [ -+ state.error === null ? null : (0, import_react41.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error), -+ (0, import_react41.createElement)( -+ SetSection, -+ { key: "new", title: t("New Project") }, -+ (0, import_react41.createElement)( -+ SetRow, -+ { label: t("Name") }, -+ (0, import_react41.createElement)("input", { -+ className: "bt-input", -+ "aria-label": t("Project name"), -+ value: name, -+ placeholder: t("e.g. Website"), -+ onChange: (event) => setName(event.target.value) -+ }) -+ ), -+ (0, import_react41.createElement)( -+ SetRow, -+ { label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") }, -+ (0, import_react41.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir }) -+ ), -+ (0, import_react41.createElement)( + const pick2 = (bot) => { + setPicked((current) => [...current, bot.id]); + setQuery(""); +@@ -8386,71 +8386,104 @@ function NewChat({ mode, roster, actions }) { + }; + const created = (bot) => { + close(); +- actions.openSession(bot.id); +- actions.showDetails(bot.id, "settings"); ++ return afterCreate({ id: bot.id, text: message, actions }); + }; + const makeGroup = async () => { + if (admin === null || picked.length === 0) return; +- const text = message.trim(); +- const room2 = await actions.createRoom([admin, ...picked], { admin, ...text ? {} : { greet: true } }); +- if (text) await actions.send(room2.id, text); ++ const text2 = message.trim(); ++ const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); ++ const participants = remotes.length === 0 ? void 0 : [ ++ ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), ++ ...remotes ++ ]; ++ const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); ++ if (text2) await actions.send(room2.id, text2); + actions.openSession(room2.id); + close(); + }; + const createGroup = () => run(makeGroup); + const sendMessage = () => run(async () => { +- const text = message.trim(); ++ const text2 = message.trim(); + if (group && admin !== null) { + await makeGroup(); + return; + } +- if (text === "" || picked.length === 0) return; ++ if (text2 === "" || picked.length === 0) return; + if (picked.length === 1) { +- await actions.send(picked[0], text); ++ await actions.send(picked[0], text2); + actions.openSession(picked[0]); + close(); + return; + } +- const room2 = await actions.createRoom(picked); +- await actions.send(room2.id, text); ++ const { members, remotes } = splitRoomPicks(roster, picked); ++ const participants = remotes.length === 0 ? void 0 : [ ++ ...members.map((id) => ({ kind: "bot", id })), ++ ...remotes ++ ]; ++ const room2 = await actions.createRoom(members, participants ? { participants } : {}); ++ await actions.send(room2.id, text2); + actions.openSession(room2.id); + close(); + }); ++ const menuOnly = mode === "new" && !compose && !group && picked.length === 0 && wanted === ""; + const options = []; + if (!group && picked.length === 0 && mode !== "search" && wanted === "") { +- options.push({ key: "new", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot"), run: createBot }); +- options.push({ key: "group", icon: (0, import_react20.createElement)(PeopleIcon), label: t("Create group chat"), run: () => { ++ if (menuOnly) { ++ options.push({ key: "chat", icon: (0, import_react34.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { ++ setCompose(true); ++ setCursor(0); ++ setTimeout(() => inputRef.current?.focus()); ++ } }); ++ } ++ options.push({ key: "new", icon: (0, import_react34.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); ++ options.push({ key: "group", icon: (0, import_react34.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { + setGroup(true); ++ setCompose(true); + setCursor(0); + inputRef.current?.focus(); + } }); ++ for (const action of newChatActionRows({ mode, group, picked, query, roster })) { ++ options.push({ ++ key: action.key, ++ icon: action.icon ? (0, import_react34.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react34.createElement)(PeopleIcon) }) : (0, import_react34.createElement)(PeopleIcon), ++ label: t(action.label), ++ hint: action.hint ? t(action.hint) : void 0, ++ run: () => { ++ const done = Promise.resolve().then(() => action.run({ close, actions, roster })); ++ done.then((note) => { ++ if (typeof note === "string" && note) actions.notify?.(note); ++ }).catch((failure) => setError(failure?.message ?? String(failure))); ++ } ++ }); ++ } + if ((roster.agents ?? []).length === 0) { +- options.push({ key: "agent", icon: (0, import_react20.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add DSH Agent"), run: () => run(async () => { ++ options.push({ key: "agent", icon: (0, import_react34.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { + await actions.addAgent(); + close(); + }) }); + } + } +- for (const bot of matches) { +- options.push({ key: bot.id, bot, label: bot.name, hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); ++ if (!menuOnly) { ++ for (const bot of matches) { ++ options.push({ key: bot.id, bot, label: pickerBotLabel(bot), hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); ++ } + } +- if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot “{name}”", { name: query.trim() }), run: createBot }); ++ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react34.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); + const active = Math.min(cursor, Math.max(options.length - 1, 0)); + const listed = options.map((option) => option.key).join("\n"); + const glide = useGlide(`${active} + ${listed}`, { axis: "y", ...QUICK_EDGES }); + useFlip(glide.box, listed, "[data-flip]"); +- const drop = (0, import_react20.useRef)(null); ++ const drop = (0, import_react34.useRef)(null); + useMorph(drop, listed); + if (draft) { +- return (0, import_react20.createElement)( ++ return (0, import_react34.createElement)( + "div", + { className: "bt-pane", style: { left: paneLeft } }, +- (0, import_react20.createElement)(CreateBot, { ++ (0, import_react34.createElement)(CreateBot, { + roster, + actions, + start: draft, +- brief: message.trim(), + canGoBack: draft.fromList, + onBack: draft.fromList ? () => { + setDraft(null); +@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + if (picked.length > 0) setPicked((current) => current.slice(0, -1)); + else if (admin !== null) setAdmin(null); + }; +- return (0, import_react20.createElement)( ++ return (0, import_react34.createElement)( + "div", +- { className: "bt-pane", style: { left: paneLeft } }, +- (0, import_react20.createElement)( ++ { ++ className: "bt-pane", ++ style: { left: paneLeft }, ++ "data-menu": menuOnly || void 0, ++ onMouseDown: menuOnly ? (event) => { ++ if (event.target === event.currentTarget) close(); ++ } : void 0 ++ }, ++ (0, import_react34.createElement)( + "div", +- { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : t("New chat") }, +- (0, import_react20.createElement)( ++ { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, ++ // The popover needs no title row: Escape or a press outside closes it. ++ menuOnly ? null : (0, import_react34.createElement)( + "div", + { className: "bt-to" }, +- (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")), +- adminBot ? (0, import_react20.createElement)( ++ (0, import_react34.createElement)("span", { className: "bt-to-label" }, t("To:")), ++ adminBot ? (0, import_react34.createElement)( + "span", + { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, +- (0, import_react20.createElement)(BotMark, { bot: adminBot, size: 16 }), ++ (0, import_react34.createElement)(BotMark, { bot: adminBot, size: 16 }), + adminBot.name, +- (0, import_react20.createElement)(AdminStar), +- (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") ++ (0, import_react34.createElement)(AdminStar), ++ (0, import_react34.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") + ) : null, +- choosingAdmin ? (0, import_react20.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react20.createElement)(AdminStar)) : null, +- pickedBots.map((bot) => (0, import_react20.createElement)( ++ choosingAdmin ? (0, import_react34.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react34.createElement)(AdminStar)) : null, ++ pickedBots.map((bot) => (0, import_react34.createElement)( + "span", + { key: bot.id, className: "bt-token" }, +- (0, import_react20.createElement)(BotMark, { bot, size: 16 }), ++ (0, import_react34.createElement)(BotMark, { bot, size: 16 }), + bot.name, +- (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") ++ (0, import_react34.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") + )), +- (0, import_react20.createElement)("input", { ++ (0, import_react34.createElement)("input", { + ref: inputRef, + autoFocus: true, + value: query, +@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + } else if (event.key === "Backspace" && query === "") unpick(); + } + }), +- group && admin !== null ? (0, import_react20.createElement)("button", { ++ group && admin !== null ? (0, import_react34.createElement)("button", { + type: "button", + className: "bt-send bt-to-create", + disabled: busy || picked.length === 0, + title: picked.length === 0 ? t("Add at least one more Bot") : void 0, + onClick: createGroup + }, t("Create group chat")) : null, +- (0, import_react20.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") ++ (0, import_react34.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") + ), +- (0, import_react20.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react20.createElement)( ++ (0, import_react34.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react34.createElement)( + "div", + { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, +- (0, import_react20.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- options.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react20.createElement)( ++ (0, import_react34.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ options.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react34.createElement)( + "button", + { + key: option.key, +@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + onMouseEnter: () => setCursor(index), + onClick: option.run + }, +- option.bot ? (0, import_react20.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react20.createElement)("span", { className: "bt-option-icon" }, option.icon), +- (0, import_react20.createElement)("span", { className: "bt-option-label" }, option.label), +- option.hint && index === active ? (0, import_react20.createElement)("span", { className: "bt-option-hint" }, option.hint) : null ++ option.bot ? (0, import_react34.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react34.createElement)("span", { className: "bt-option-icon" }, option.icon), ++ // An action row's description sits under its title and is always there (#283): a long line ++ // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. ++ // A Bot row's short hint stays beside the name and shows on the active row only. ++ option.bot || !option.hint ? (0, import_react34.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react34.createElement)("span", { className: "bt-option-copy" }, (0, import_react34.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react34.createElement)("span", { className: "bt-option-sub" }, option.hint)), ++ option.bot && option.hint && index === active ? (0, import_react34.createElement)("span", { className: "bt-option-hint" }, option.hint) : null + )) + )), +- (0, import_react20.createElement)("div", { className: "bt-nc-spacer" }), +- error ? (0, import_react20.createElement)("div", { className: "bt-error" }, error) : null, +- (0, import_react20.createElement)( ++ menuOnly ? null : (0, import_react34.createElement)("div", { className: "bt-nc-spacer" }), ++ error && fix ? (0, import_react34.createElement)( + "div", -+ { className: "bt-actions" }, -+ (0, import_react41.createElement)("button", { -+ type: "button", -+ className: "bt-primary bt-with-icon", -+ disabled: name.trim() === "", -+ onClick: () => { -+ void create(); -+ } -+ }, (0, import_react41.createElement)(PlusIcon), t("New Project")) -+ ) -+ ), -+ // The list is rendered here rather than as a child element: it holds no state of its own, -+ // and the page keeps one reload path for both halves. -+ ...ProjectsList({ projects: state.projects, roster, call: call4, reload }) -+ ]; -+} ++ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, ++ (0, import_react34.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), ++ (0, import_react34.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ ++ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react34.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { ++ void copyInvite(bot); ++ } }, t("Copy invite link for {name}", { name: bot.name })) : null, ++ (0, import_react34.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) ++ ])), ++ fixNote ? (0, import_react34.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, ++ inviteLink ? (0, import_react34.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null ++ ) : error ? (0, import_react34.createElement)("div", { className: "bt-error" }, error) : null, ++ menuOnly ? null : (0, import_react34.createElement)( + "div", + { className: "bt-nc-compose" }, +- (0, import_react20.createElement)( ++ (0, import_react34.createElement)( + "div", + { className: "bt-nc-card" }, +- (0, import_react20.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react20.createElement)(PlusIcon)), +- (0, import_react20.createElement)("textarea", { ++ (0, import_react34.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react34.createElement)(PlusIcon)), ++ (0, import_react34.createElement)("textarea", { + rows: 1, + value: message, + placeholder, +@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + } + } + }), +- (0, import_react20.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react20.createElement)(ArrowUpIcon)) ++ (0, import_react34.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react34.createElement)(ArrowUpIcon)) + ) + ) + ) +@@ -8577,18 +8577,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); + } - // src/client/context-menu.js --var import_react29 = require("react"); -+var import_react42 = require("react"); - function ContextMenu({ menu, roster, actions, leaving = false }) { - useEscape(() => actions.closeMenu()); -- (0, import_react29.useEffect)(() => { -+ (0, import_react42.useEffect)(() => { - const onDown = (event) => { - if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); - }; - window.addEventListener("mousedown", onDown); - return () => window.removeEventListener("mousedown", onDown); - }, []); -- const ref = (0, import_react29.useRef)(null); -- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); -- (0, import_react29.useLayoutEffect)(() => { -+ const structure = useSidebarStructure(); -+ const ref = (0, import_react42.useRef)(null); -+ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react42.useLayoutEffect)(() => { - const box = ref.current?.getBoundingClientRect(); - if (!box) return; - setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); - }, [menu.x, menu.y]); -- if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react29.createElement)(AgentMenu, { menu, roster, actions, leaving }); -+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react42.createElement)(AgentMenu, { menu, roster, actions, leaving }); - const bot = roster.byId[menu.id]; - const room2 = roster.roomsById[menu.id]; - const entry = bot ?? room2; -@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { - const isMainBot = isMainOf(roster, menu.id); - const isTile = roster.mainBotId === menu.id; - const mainCount = (roster.mainBotIds ?? []).length; -- const groups = [ -+ const groups = bot?.remote ? [ -+ [ -+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -+ ], -+ [ -+ [t("Bot settings"), GearIcon, () => actions.openSettings("bots", bot.id)] -+ ], -+ pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]), -+ [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(currentPart(bot))]] -+ ] : [ - [ - isTile ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })], - [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] - ], - [ -- bot ? [t("Rename Bot"), PencilIcon, () => actions.renameBot(bot.id)] : null, -+ // One way to edit a Bot (#385): its name is edited in Edit Bot too. - bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, - room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, - room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { - bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, - bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null - ], -+ bot ? pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]) : [], - [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(bot ? currentPart(bot) : menu.id)]], - [ - isMainBot ? null : [t("Hide from sidebar"), HideIcon, () => actions.setFlags(menu.id, { hidden: true })], - isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true] - ] -- ].map((group) => group.filter(Boolean)).filter((group) => group.length > 0); -- return (0, import_react29.createElement)( -+ ]; -+ const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); -+ return (0, import_react42.createElement)( + // src/client/exchange.js +-var import_react21 = require("react"); ++var import_react35 = require("react"); + var SHEET_EXIT_MS = 220; ++function exchangeText(text2) { ++ if (typeof text2 !== "string") return ""; ++ const CLOSE = ""; ++ let body = text2; ++ for (let previous; previous !== body; ) { ++ previous = body; ++ const open = body.indexOf(" open ? body.slice(body.indexOf(">", open) + 1, close) : body; ++ body = stripFence(body.slice(0, open) + inner + (close > open ? body.slice(close + CLOSE.length) : "")); ++ } ++ return stripFence(stripTrace(body.replace(/^\s+/, ""))).trim(); ++} ++function ExchangeLine({ entry, roster, actions }) { ++ const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; ++ return (0, import_react35.createElement)( ++ "div", ++ { className: "bt-group-msg" }, ++ (0, import_react35.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), ++ (0, import_react35.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react35.createElement)("span", { className: "bt-lead" }, (0, import_react35.createElement)(BotMark, { bot, size: 22 })) }) ++ ); ++} + function ExchangeDialog({ pair, roster, actions }) { +- const [entries2, setEntries] = (0, import_react21.useState)(null); +- const [leaving, setLeaving] = (0, import_react21.useState)(false); ++ const [entries2, setEntries] = (0, import_react35.useState)(null); ++ const [leaving, setLeaving] = (0, import_react35.useState)(false); + const close = () => { + if (leaving) return; + setLeaving(true); + setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS); + }; + useEscape(close); +- (0, import_react21.useEffect)(() => { ++ (0, import_react35.useEffect)(() => { + let stop = false; + let timer; + let seen = null; +@@ -8612,49 +8612,42 @@ function ExchangeDialog({ pair, roster, actions }) { + const right = roster.byId[pair[1]]; + let lastDay = 0; + const paneLeft = usePaneLeft(); +- return (0, import_react21.createElement)( ++ return (0, import_react35.createElement)( "div", - { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, -- groups.map((group, index) => (0, import_react29.createElement)( -- import_react29.Fragment, -+ shown.map((group, index) => (0, import_react42.createElement)( -+ import_react42.Fragment, - { key: index }, -- index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { -+ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", { - key: label, - type: "button", - role: "menuitem", -@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { - actions.closeMenu(); - void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); - } -- }, (0, import_react29.createElement)(Icon), label)) -+ }, (0, import_react42.createElement)(Icon), label)) - )) + { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, +- (0, import_react21.createElement)( ++ (0, import_react35.createElement)( + "div", + { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, +- (0, import_react21.createElement)("div", { className: "bt-head" }, (0, import_react21.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-head" }, (0, import_react35.createElement)( + "span", + { className: "bt-pill", style: { cursor: "default" } }, +- left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null, ++ left ? (0, import_react35.createElement)(BotMark, { bot: left, size: 22 }) : null, + left?.name ?? "?", +- (0, import_react21.createElement)("span", { className: "bt-swap" }, "⇄"), +- right ? (0, import_react21.createElement)(BotMark, { bot: right, size: 22 }) : null, ++ (0, import_react35.createElement)("span", { className: "bt-swap" }, "⇄"), ++ right ? (0, import_react35.createElement)(BotMark, { bot: right, size: 22 }) : null, + right?.name ?? "?" + )), +- (0, import_react21.createElement)( ++ (0, import_react35.createElement)( + "div", + { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, +- entries2 === null ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { +- const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; ++ entries2 === null ? (0, import_react35.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { + const showDay = entry.time - lastDay > 30 * 60 * 1e3; + lastDay = entry.time; +- return (0, import_react21.createElement)( +- import_react21.Fragment, ++ return (0, import_react35.createElement)( ++ import_react35.Fragment, + { key: entry.id }, +- showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, +- (0, import_react21.createElement)( +- "div", +- { className: "bt-group-msg" }, +- (0, import_react21.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), +- (0, import_react21.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react21.createElement)("span", { className: "bt-lead" }, (0, import_react21.createElement)(BotMark, { bot, size: 22 })) }) +- ) ++ showDay ? (0, import_react35.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, ++ (0, import_react35.createElement)(ExchangeLine, { entry, roster, actions }) + ); + }) + ), +- (0, import_react21.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) ++ (0, import_react35.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react35.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) + ) ); } - function AgentMenu({ menu, roster, actions, leaving }) { -- const ref = (0, import_react29.useRef)(null); -- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); -- (0, import_react29.useLayoutEffect)(() => { -+ const ref = (0, import_react42.useRef)(null); -+ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react42.useLayoutEffect)(() => { - const box = ref.current?.getBoundingClientRect(); - if (!box) return; - setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { - [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] - ] - ]; -- return (0, import_react29.createElement)( -+ return (0, import_react42.createElement)( + + // src/client/voice.js +-var import_react22 = require("react"); +-var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); ++var import_react36 = require("react"); + var VOICE_LABELS = { + listening: "Listening", + thinking: "Thinking", +@@ -8665,14 +8665,14 @@ var VOICE_LABELS = { + }; + var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; + function useMicLevel(enabled) { +- const [level, setLevel] = (0, import_react22.useState)(0); +- (0, import_react22.useEffect)(() => { ++ const [level, setLevel] = (0, import_react36.useState)(0); ++ (0, import_react36.useEffect)(() => { + if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; + let stream = null; + let audio = null; + let frame = 0; + let stopped = false; +- navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { ++ void navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { + if (stopped) { + media.getTracks().forEach((track) => track.stop()); + return; +@@ -8716,18 +8716,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; + return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; + }); +- const [speech] = (0, import_react22.useState)(() => actions.speech()); +- const [phase, setPhase] = (0, import_react22.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); +- const [catalog, setCatalog] = (0, import_react22.useState)(null); +- const [heard, setHeard] = (0, import_react22.useState)(""); +- const [said, setSaid] = (0, import_react22.useState)(""); +- const [note, setNote] = (0, import_react22.useState)(""); +- const [serviceLevel, setServiceLevel] = (0, import_react22.useState)(0); +- const [cycle, setCycle] = (0, import_react22.useState)(0); +- const waiting = (0, import_react22.useRef)(null); +- const reply = (0, import_react22.useRef)(lastReply); ++ const [speech] = (0, import_react36.useState)(() => actions.speech()); ++ const [phase, setPhase] = (0, import_react36.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); ++ const [catalog, setCatalog] = (0, import_react36.useState)(null); ++ const [heard, setHeard] = (0, import_react36.useState)(""); ++ const [said, setSaid] = (0, import_react36.useState)(""); ++ const [note, setNote] = (0, import_react36.useState)(""); ++ const [serviceLevel, setServiceLevel] = (0, import_react36.useState)(0); ++ const [cycle, setCycle] = (0, import_react36.useState)(0); ++ const waiting = (0, import_react36.useRef)(null); ++ const reply = (0, import_react36.useRef)(lastReply); + reply.current = lastReply; +- const turn = (0, import_react22.useRef)(0); ++ const turn = (0, import_react36.useRef)(0); + const paneLeft = usePaneLeft(); + const close = () => { + turn.current += 1; +@@ -8735,24 +8735,24 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + actions.closeVoice(); + }; + useEscape(close); +- const submit = (text) => { +- setHeard(text); ++ const submit = (text2) => { ++ setHeard(text2); + setPhase("thinking"); + waiting.current = reply.current.split("|")[0]; +- void actions.send(sessionId, text.trim()).catch(() => setPhase("listening")); ++ void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); + }; +- const dictation = useDictation((text, final) => { +- setHeard(text); ++ const dictation = useDictation((text2, final) => { ++ setHeard(text2); + if (!final) return; + dictation.stop(); +- submit(text); ++ submit(text2); + }); +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (!note) return void 0; + const timer = setTimeout(() => setNote(""), 2600); + return () => clearTimeout(timer); + }, [note]); +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (!speech) return void 0; + let live = true; + speechCatalog(speech).then(({ catalog: next, usable }) => { +@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + }; + }, [speech]); + const listen = phase === "listening"; +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (speech || !listen || dictation.listening) return void 0; + const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { + } }), 250); + return () => clearTimeout(timer); + }, [listen, dictation.listening]); +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (phase !== "listening") dictation.stop(); + }, [phase]); +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (!speech || !catalog || !listen) return void 0; + const own = turn.current; + let capture = null; +@@ -8789,9 +8789,9 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + setPhase("transcribing"); + try { + const wave = await current.finish(catalog.maxDurationSeconds); +- const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; ++ const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; + if (turn.current !== own) return; +- if (text) submit(text); ++ if (text2) submit(text2); + else setPhase("listening"); + } catch (error) { + if (turn.current !== own) return; +@@ -8838,12 +8838,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + setServiceLevel(0); + }; + }, [speech, catalog, listen, cycle]); +- (0, import_react22.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (phase !== "thinking" || running || waiting.current === null) return; +- const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; +- if (count === waiting.current || !text.trim()) return; ++ const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; ++ if (count === waiting.current || !text2.trim()) return; + waiting.current = null; +- const spoken = plainSpeech(text); ++ const spoken = speechOf(text2, botNames(roster)); + setSaid(spoken); + setHeard(""); + if (!window.speechSynthesis || !spoken) { +@@ -8858,7 +8858,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + window.speechSynthesis.cancel(); + window.speechSynthesis.speak(utterance); + }, [phase, running, lastReply]); +- (0, import_react22.useEffect)(() => () => window.speechSynthesis?.cancel(), []); ++ (0, import_react36.useEffect)(() => () => window.speechSynthesis?.cancel(), []); + const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); + const level = speech ? serviceLevel : micLevel; + const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; +@@ -8875,291 +8875,678 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } + const openSetup = phase === "setup" ? actions.voiceSetup() : null; + const caption = phase === "speaking" ? said : heard; + const shownNote = note || dictation.note; +- return (0, import_react22.createElement)( ++ return (0, import_react36.createElement)( "div", - { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, -- groups.map((group, index) => (0, import_react29.createElement)( -- import_react29.Fragment, -+ groups.map((group, index) => (0, import_react42.createElement)( -+ import_react42.Fragment, - { key: index }, -- index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { -+ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", { - key: label, + { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, +- (0, import_react22.createElement)( ++ (0, import_react36.createElement)( + "div", + { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, +- (0, import_react22.createElement)( ++ (0, import_react36.createElement)( + "div", + { className: "bt-voice-orb", style: { "--bt-level": level } }, +- (0, import_react22.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), +- (0, import_react22.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), +- bot ? (0, import_react22.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null ++ (0, import_react36.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), ++ (0, import_react36.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), ++ bot ? (0, import_react36.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react36.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null + ), +- (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name), +- (0, import_react22.createElement)( ++ (0, import_react36.createElement)("div", { className: "bt-voice-name" }, name), ++ (0, import_react36.createElement)( + "div", + { className: "bt-voice-status", role: "status" }, +- phase === "thinking" ? (0, import_react22.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react22.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" ++ phase === "thinking" ? (0, import_react36.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react36.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" + ), +- (0, import_react22.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), +- openSetup ? (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { ++ (0, import_react36.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), ++ openSetup ? (0, import_react36.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { + close(); + openSetup(); + } }, t("Set up voice input")) : null, +- shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null ++ shownNote ? (0, import_react36.createElement)("div", { className: "bt-voice-note" }, shownNote) : null + ), +- (0, import_react22.createElement)( ++ (0, import_react36.createElement)( + "div", + { className: "bt-voice-bar" }, +- (0, import_react22.createElement)("button", { ++ (0, import_react36.createElement)("button", { type: "button", - role: "menuitem", -@@ -10968,16 +10968,31 @@ function AgentMenu({ menu, roster, actions, leaving }) { - actions.closeMenu(); - void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); - } -- }, (0, import_react29.createElement)(Icon), label)) -+ }, (0, import_react42.createElement)(Icon), label)) - )) + className: "bt-voice-btn", + "aria-pressed": phase === "paused", + disabled: blocked, + "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), + onClick: togglePause +- }, (0, import_react22.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react22.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), +- (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react22.createElement)(CloseIcon)) ++ }, (0, import_react36.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react36.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react36.createElement)(CloseIcon)) + ) ); } - -+// src/client/pane-width.js -+var DEFAULT_WIDTH = 520; -+var DETAILS_WIDTH = 380; -+var DETAILS_MAX = 520; -+var MIN_WIDTH = 360; -+var MIN_CHAT = 420; -+var MIN_DETAILS = 240; -+function panelWidthRange(view, viewport) { -+ const room2 = Math.max(0, viewport - MIN_CHAT); -+ if (view === "browser") return [MIN_WIDTH, Math.max(MIN_WIDTH, room2)]; -+ const low = Math.max(MIN_DETAILS, Math.min(DETAILS_WIDTH, room2)); -+ return [low, Math.max(low, Math.min(DETAILS_MAX, room2))]; + +-// src/client/group-settings.js +-var import_react23 = require("react"); +-var ROOM_MODES = [ +- ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], +- ["everyone", "Everyone", "Every member answers in turn, admin first."] +-]; +-var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; +-function roomSubtitle(room2, roster) { +- const admin = roster.byId[room2.admin]; +- return [t("{count} members", { count: room2.members.length }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); ++// src/client/workflows.js ++var import_react37 = require("react"); ++var READ_MS = 8e3; ++var POLL_MS6 = 2e3; ++var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; ++var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; ++function workflowCallMissing(failure) { ++ const code = failure && typeof failure === "object" ? String(failure.code ?? "") : ""; ++ const message = typeof failure === "string" ? failure : String(failure?.message ?? ""); ++ return code === "bot/not-found" || /Unknown FlyBot endpoint workflow\//i.test(message); ++} ++function workflowCallError(failure, fallback = "Could not read workflows") { ++ if (workflowCallMissing(failure)) return WORKFLOW_PLUGIN_HINT; ++ if (typeof failure === "string" && failure.trim()) return failure.trim(); ++ const message = typeof failure?.message === "string" ? failure.message.trim() : ""; ++ return message || fallback; ++} ++function settleWorkflowRead(value, fallback = "Could not read workflows") { ++ if (!value || value.ok === false || !Array.isArray(value.workflows)) { ++ const raw = typeof value?.error === "string" && value.error.trim() ? value.error : ""; ++ const error = workflowCallMissing(value) || workflowCallMissing(raw) ? WORKFLOW_PLUGIN_HINT : raw || fallback; ++ return { phase: "error", error, pack: null }; ++ } ++ const templates = Array.isArray(value.templates) ? value.templates : []; ++ const workflows = value.workflows; ++ const pack = { ...value, templates, workflows }; ++ if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack }; ++ return { phase: "ready", error: "", pack }; ++} ++function readWorkflows(call4, { timeoutMs = READ_MS } = {}) { ++ return new Promise((resolve) => { ++ let settled = false; ++ const finish = (value) => { ++ if (settled) return; ++ settled = true; ++ clearTimeout(timer); ++ resolve(value); ++ }; ++ const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs); ++ Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null })); ++ }); + } +-function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { +- const [adding, setAdding] = (0, import_react23.useState)(false); +- const [confirming, setConfirming] = (0, import_react23.useState)(false); +- (0, import_react23.useEffect)(() => { +- if (!confirming) return void 0; +- const timer = setTimeout(() => setConfirming(false), 4e3); +- return () => clearTimeout(timer); +- }, [confirming]); +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const outside = roster.bots.filter((bot) => !room2.members.includes(bot.id)); +- const admin = roster.byId[room2.admin]; +- const mode = shownMode(room2, roster); +- const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; +- const update = (patch) => run(actions.updateRoom(room2.id, patch)); +- const openBot = (id) => { +- actions.closeSettings(); +- actions.openSession(id); ++function moveNode(nodes, id, x, y) { ++ return nodes.map((node) => node.id === id ? { ...node, x, y } : node); +} -+ - // src/client/storage.js - var STORAGE = { - unread: "ds-bot:unread", - reactions: "ds-bot:reactions", - seen: "ds-bot:seen", -+ friendLines: "ds-bot:friend-lines", - prefs: "ds-bot:prefs", - surface: "ds-bot:surface", - agentSessions: "ds-bot:agent-sessions", -@@ -11021,261 +11021,25 @@ function migrateStorage(storage = globalThis.localStorage) { - const value = storage.getItem(legacy); - if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); - storage.removeItem(legacy); -- } catch { -- } -- } -- } -- try { -- migrateBrowsers(storage); -- } catch { -- } -- try { -- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -- } -- } catch { -- } --} -- --// src/client/member-status.js --var import_react30 = require("react"); --var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; --var ROWS_MAX = 4; --function memberWork(room2, work = NO_WORK) { -- const here = work.rooms?.[room2.id] ?? []; -- return room2.members.map((id) => { -- const approvals = (work.approvals ?? []).filter((entry) => entry.botId === id); -- const local = here.find((entry) => entry.botId === id); -- const doing = local ? { ...local, roomId: room2.id } : work.bots?.[id]; -- return { id, approvals, doing, here: local !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; -- }); --} --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] })); --} --function splitBusy(busy) { -- const speaking = (entry) => entry.state === "work" && entry.here && entry.doing.speaking === true; -- const rows = busy.filter((entry) => entry.state === "work" && entry.here && !speaking(entry)).slice(0, ROWS_MAX); -- return { rows, rest: busy.filter((entry) => !rows.includes(entry) && !speaking(entry)) }; --} --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, since: entry.since }] : []; -- }); --} --function summaryText(busy) { -- const asks = busy.filter((entry) => entry.state === "ask"); -- const workers = busy.filter((entry) => entry.state === "work"); -- const names = asks.slice(0, 2).map((entry) => entry.bot.name); -- const waiting = asks.length === 0 ? "" : asks.length === 1 ? t("{name} is waiting for your approval", { name: names[0] }) : asks.length === 2 ? t("{first} and {second} are waiting for your approval", { first: names[0], second: names[1] }) : t("{first}, {second} and {count} more are waiting for your approval", { first: names[0], second: names[1], count: asks.length - 2, total: asks.length }); -- const first = workers[0]; -- const doing = first ? `${first.bot.name} · ${workPhrase(first.doing.tool)}…${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; -- return { waiting, doing }; --} --function Face({ entry, size }) { -- return (0, import_react30.createElement)( -- "span", -- { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, -- (0, import_react30.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) -- ); --} --function WorkRow({ bot, tool, label, at, now, onOpen }) { -- const text = `${label} · ${workPhrase(tool)}…`; -- const elapsed = typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; -- return (0, import_react30.createElement)( -- "button", -- { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, -- (0, import_react30.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }), -- (0, import_react30.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react30.createElement)(Shimmer, null, text)), -- elapsed ? (0, import_react30.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -- ); --} --function SummaryLine({ busy, onOpen }) { -- const { waiting, doing } = summaryText(busy); -- return (0, import_react30.createElement)( -- "div", -- { className: "bt-event bt-mw-dock" }, -- (0, import_react30.createElement)( -- "button", -- { type: "button", className: "bt-event-whole bt-mw-line", "aria-label": [waiting, doing].filter(Boolean).join(" · "), onClick: onOpen }, -- (0, import_react30.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react30.createElement)(Face, { key: entry.id, entry, size: 16 }))), -- waiting ? (0, import_react30.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, -- waiting && doing ? (0, import_react30.createElement)("span", { "aria-hidden": true }, "·") : null, -- doing ? (0, import_react30.createElement)("span", { className: "bt-event-name" }, (0, import_react30.createElement)(Shimmer, null, doing)) : null -- ) -- ); --} --function MemberDock({ sessionId, useRoster, useWork, actions }) { -- const roster = useRoster((value) => value); -- const work = useWork((value) => value); -- const room2 = roster.roomsById[sessionId]; -- const bot = room2 ? void 0 : roster.byId[sessionId]; -- const ownChat = bot !== void 0 && work.bots?.[bot.id] !== void 0 && work.bots[bot.id].roomId === void 0; -- const busy = room2 ? busyMembers(room2, work, roster) : []; -- const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : []; -- const shown = busy.length > 0 || groups.length > 0; -- const now = useNow2(shown); -- const ref = (0, import_react30.useRef)(null); -- useFlowColumn(ref, shown, sessionId); -- if (!shown) return null; -- if (room2) { -- const { rows, rest } = splitBusy(busy); -- const open = () => actions.showDetails(sessionId); -- return (0, import_react30.createElement)( -- "div", -- { ref, className: "bt-mw-dock-stack" }, -- rows.map((entry) => (0, import_react30.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })), -- rest.length > 0 ? (0, import_react30.createElement)(SummaryLine, { busy: rest, onOpen: open }) : null -- ); -- } -- return (0, import_react30.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react30.createElement)(WorkRow, { -- key: item.room.id, -- bot, -- tool: item.tool, -- label: t("in {name}", { name: item.room.name }), -- at: item.since, -- now, -- onOpen: () => actions.openSession(item.room.id) -- }))); --} --function useFlowColumn(ref, active, key) { -- (0, import_react30.useLayoutEffect)(() => { -- const node = ref.current; -- const flow = document.querySelector("[data-chat-flow]"); -- if (!active || !node || !flow) return void 0; -- const place = () => { -- node.style.marginInlineStart = "0px"; -- const column = flow.getBoundingClientRect(); -- node.style.marginInlineStart = `${Math.round(column.left - node.getBoundingClientRect().left)}px`; -- node.style.width = `${Math.round(column.width)}px`; -- }; -- place(); -- const observer = new ResizeObserver(place); -- for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); -- return () => observer.disconnect(); -- }, [active, key]); --} --function useNow2(active) { -- const [now, setNow] = (0, import_react30.useState)(Date.now); -- (0, import_react30.useEffect)(() => { -- if (!active) return void 0; -- const timer = setInterval(() => setNow(Date.now()), 3e4); -- return () => clearInterval(timer); -- }, [active]); -- return now; --} --var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; --function ApprovalCard({ approval, bot, actions }) { -- const [busy, setBusy] = (0, import_react30.useState)(false); -- const answer = (choice) => { -- setBusy(true); -- void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); ++function connectNodes(edges, from, to) { ++ if (!from || !to || from === to) return edges; ++ if (edges.some((edge) => edge.from === from && edge.to === to)) return edges; ++ return edges.concat([{ from, to }]); ++} ++function dropBot(nodes, bot, x, y) { ++ const ids = new Set(nodes.map((node) => node.id)); ++ let n = nodes.length + 1; ++ while (ids.has(`n${n}`)) n += 1; ++ return nodes.concat([{ id: `n${n}`, name: bot.name, botId: bot.id, role: bot.role ?? "", x, y }]); ++} ++function removeNode(nodes, edges, id) { ++ return { ++ nodes: nodes.filter((node) => node.id !== id), ++ edges: edges.filter((edge) => edge.from !== id && edge.to !== id) + }; +- const remove = () => { +- if (!confirming) { +- setConfirming(true); +- return; ++} ++function chainOf(item) { ++ if (typeof item?.chain === "string" && item.chain) return item.chain; ++ if (!Array.isArray(item?.layers) || !Array.isArray(item?.nodes)) return ""; ++ const name = (id) => item.nodes.find((node) => node.id === id)?.name ?? id; ++ return item.layers.map((layer) => layer.map(name).join(" ‖ ")).join(" → "); ++} ++function openedGraph(pack, opened) { ++ if (!pack || !opened) return null; ++ if (opened.source === "own") return (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; ++ return (pack.templates ?? []).find((item) => item.id === opened.id) ?? (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; ++} ++function statusLabel2(status) { ++ if (status === "running") return t("Running"); ++ if (status === "done") return t("Done"); ++ if (status === "failed") return t("Failed"); ++ if (status === "idle") return t("Idle"); ++ if (status === "skipped") return t("Skipped"); ++ return t("Queued"); ++} ++function hitNode(event) { ++ const direct = event.target?.dataset?.node ? event.target : null; ++ const nested = event.target?.closest?.("[data-node]"); ++ return (nested ?? direct)?.dataset?.node || ""; ++} ++var WIRE_SPAN = 480; ++var NODE_WIRE_OUT = 56; ++var CARD_HALF = 84; ++var CARD_H = 72; ++var FRAME_SIDE = 108; ++var FRAME_LABEL = 18; ++var FRAME_FOOT = 10; ++var MIN_FIT = 0.5; ++function wirePath(from, to) { ++ const x1 = from.x + WIRE_SPAN; ++ const y1 = from.y + NODE_WIRE_OUT; ++ const x2 = to.x + WIRE_SPAN; ++ const y2 = to.y; ++ const mid = (y1 + y2) / 2; ++ return `M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`; ++} ++function fitScale(nodes, width) { ++ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); ++ if (!(width > 0) || reach === 0) return 1; ++ return Math.min(1, Math.max(MIN_FIT, width / 2 / reach)); ++} ++function fitLayout(nodes, width) { ++ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); ++ const scale = fitScale(nodes, width); ++ const spare = width > 0 && reach > 0 ? reach * scale - width / 2 : 0; ++ const pad2 = spare > 1e-6 ? Math.ceil(spare) : 0; ++ const centre = width > 0 ? pad2 + width / 2 : 0; ++ return { ++ reach, ++ scale, ++ pad: pad2, ++ left: centre - reach * scale, ++ right: centre + reach * scale, ++ scrollWidth: Math.max(width + pad2, centre + reach * scale) ++ }; ++} ++function measureCanvasWidth(el) { ++ const width = el?.clientWidth; ++ return Number.isFinite(width) && width > 0 ? width : 0; ++} ++function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) { ++ const gesture = (0, import_react37.useRef)(null); ++ const canvasRef = (0, import_react37.useRef)(null); ++ const [measuredWidth, setCanvasWidth] = (0, import_react37.useState)(0); ++ const nodes = graph.nodes ?? []; ++ const edges = graph.edges ?? []; ++ const status = graph.run?.nodeStatus ?? null; ++ const locals = (roster?.bots ?? []).filter((bot) => bot && bot.remote !== true && !nodes.some((node) => node.botId === bot.id)); ++ const height = Math.max(420, ...nodes.map((node) => (node.y ?? 0) + 150), 0); ++ const canvasWidth = measuredWidth || knownWidth; ++ const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth); ++ const drawnHeight = Math.round(height * scale); ++ (0, import_react37.useEffect)(() => { ++ const el = canvasRef.current; ++ const measure = () => { ++ const width = measureCanvasWidth(el); ++ if (width > 0 && Math.abs(width - canvasWidth) > 0.5) setCanvasWidth(width); ++ }; ++ measure(); ++ if (typeof ResizeObserver !== "function" || !el) return void 0; ++ const observer = new ResizeObserver(measure); ++ observer.observe(el); ++ return () => observer.disconnect(); ++ }, [canvasWidth]); ++ const end = (event) => { ++ const current = gesture.current; ++ gesture.current = null; ++ if (!current || !editing) return; ++ if (current.kind === "move") { ++ const x = Math.round(current.x + (event.clientX - current.ox) / scale); ++ const y = Math.round(current.y + (event.clientY - current.oy) / scale); ++ onCommit?.({ nodes: moveNode(nodes, current.id, x, y), edges }); ++ } else if (current.kind === "wire") { ++ const next = connectNodes(edges, current.from, hitNode(event)); ++ if (next !== edges) onCommit?.({ nodes, edges: next }); ++ } else if (current.kind === "add" && event.target?.dataset?.plane) { ++ const x = Number.isFinite(event.offsetX) ? Math.round(event.offsetX / scale) : 24; ++ const y = Number.isFinite(event.offsetY) ? Math.round(event.offsetY / scale) : 24; ++ onCommit?.({ nodes: dropBot(nodes, current.bot, x, y), edges }); + } +- void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { +- if (done) onDeleted?.(); +- }); + }; +- return (0, import_react23.createElement)( ++ return (0, import_react37.createElement)( + "div", +- { className: "bt-bs" }, +- (0, import_react23.createElement)( ++ { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end }, ++ editing ? (0, import_react37.createElement)( + "div", +- { className: "bt-bs-hero" }, +- (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-bs-who" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), +- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Profile") }, +- (0, import_react23.createElement)( +- Row, +- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, +- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) +- ), +- (0, import_react23.createElement)( +- Row, +- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, +- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) +- ) +- ), +- (0, import_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.")}`) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Working folder") }, +- (0, import_react23.createElement)( ++ { 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", { ++ key: bot.id, ++ type: "button", ++ "data-bot": bot.id, ++ onPointerDown: (event) => { ++ event.preventDefault?.(); ++ gesture.current = { kind: "add", bot }; ++ } ++ }, bot.name)) ++ ) : null, ++ // A node's system prompt is the run Bot's prompt (plugins/workflow ++ // nodeConfig). The sidebar panel has this field, so the Settings editor ++ // writes the same graph instead of only drawing it (#1393). ++ editing ? (0, import_react37.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react37.createElement)( ++ "label", ++ { key: node.id, className: "bt-wf-prompt" }, ++ (0, import_react37.createElement)("span", null, `${node.name} · ${t("System prompt")}`), ++ (0, import_react37.createElement)("textarea", { ++ "data-prompt": node.id, ++ rows: 3, ++ defaultValue: node.prompt || "", ++ onPointerDown: (event) => event.stopPropagation?.(), ++ onBlur: (event) => { ++ const prompt = event.target?.value ?? ""; ++ if (prompt === (node.prompt || "")) return; ++ onCommit?.({ nodes: nodes.map((item) => item.id === node.id ? { ...item, prompt } : item), edges }); ++ } ++ }) ++ ))) : null, ++ (0, import_react37.createElement)( ++ "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 }) }) +- ) +- ), +- (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)( ++ { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, ++ (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)) ++ { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, ++ (0, import_react37.createElement)( ++ "svg", ++ { ++ className: "bt-wf-wires", ++ "aria-hidden": true, ++ width: WIRE_SPAN * 2, ++ height, ++ viewBox: `0 0 ${WIRE_SPAN * 2} ${height}` ++ }, ++ (0, import_react37.createElement)( ++ "defs", ++ null, ++ (0, import_react37.createElement)("marker", { ++ id: "bt-wf-arrow", ++ viewBox: "0 0 10 10", ++ refX: "8", ++ refY: "5", ++ markerWidth: "6", ++ markerHeight: "6", ++ orient: "auto" ++ }, (0, import_react37.createElement)("path", { d: "M0 0 10 5 0 10z" })) ++ ), ++ edges.map((edge) => { ++ const from = nodes.find((node) => node.id === edge.from); ++ const to = nodes.find((node) => node.id === edge.to); ++ if (!from || !to) return null; ++ return (0, import_react37.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" }); ++ }) ++ ), ++ (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => { ++ const members = layer.map((id) => nodes.find((node) => node.id === id)).filter(Boolean); ++ if (members.length < 2) return null; ++ const left = Math.min(...members.map((node) => node.x ?? 0)) - FRAME_SIDE; ++ const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE; ++ const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL; ++ const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT; ++ return (0, import_react37.createElement)("div", { ++ key: layer.join("+"), ++ className: "bt-wf-par", ++ style: { left, top, width: right - left, height: bottom - top } ++ }, (0, import_react37.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join"))); ++ }), ++ nodes.map((node) => { ++ const state = status ? status[node.id] ?? "queued" : "idle"; ++ return (0, import_react37.createElement)( ++ "div", ++ { ++ key: node.id, ++ "data-node": node.id, ++ className: `bt-wf-node ${state}${editing ? " editing" : ""}`, ++ style: { left: (node.x ?? 0) - CARD_HALF, top: node.y ?? 0 }, ++ onPointerDown: (event) => { ++ if (!editing || event.target?.dataset?.port || event.target?.dataset?.remove) return; ++ gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 }; ++ } ++ }, ++ editing ? (0, import_react37.createElement)("button", { ++ type: "button", ++ className: "bt-wf-port", ++ "data-port": "out", ++ "aria-label": node.name, ++ onPointerDown: (event) => { ++ event.preventDefault?.(); ++ event.stopPropagation?.(); ++ gesture.current = { kind: "wire", from: node.id }; ++ } ++ }) : null, ++ editing ? (0, import_react37.createElement)("button", { ++ type: "button", ++ className: "bt-wf-remove", ++ "data-remove": node.id, ++ "aria-label": t("Remove"), ++ onClick: (event) => { ++ event.stopPropagation?.(); ++ const next = removeNode(nodes, edges, node.id); ++ onCommit?.(next); ++ } ++ }, "×") : null, ++ (0, import_react37.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)), ++ (0, import_react37.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)), ++ (0, import_react37.createElement)( ++ "span", ++ { className: "bt-wf-copy" }, ++ (0, import_react37.createElement)("span", { className: "bt-wf-name" }, node.name), ++ (0, import_react37.createElement)("span", { className: "bt-wf-role" }, node.role ?? "") ++ ) ++ ); ++ }) ++ ) + ) + ) + ); + } +-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)); - }; -- return (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, -- (0, import_react30.createElement)("div", { className: "bt-mw-tool" }, approval.tool), -- approval.detail?.text ? (0, import_react30.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, -- approval.reason ? (0, import_react30.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null, -- approval.detail?.code ? (0, import_react30.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null, -- approval.answerable ? (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-actions" }, -- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")), -- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")) -- ) : (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-actions" }, -- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) -- ) -- ); --} --function MemberWork({ entry, bot, room: room2, roster, actions, now }) { -- const [stopping, setStopping] = (0, import_react30.useState)(false); -- if (entry.state === "ask") { -- return (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-member" }, -- (0, import_react30.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_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) -- ); -- } -- if (entry.state !== "work") return null; -- const where = entry.doing.roomId === room2.id ? "" : entry.doing.roomId ? t("in {name}", { name: roster.roomsById[entry.doing.roomId]?.name ?? t("a group chat") }) : t("in its own chat"); -- const stop = () => { -- setStopping(true); -- void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); +- 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); - }; -- return (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-member" }, -- (0, import_react30.createElement)( -- "div", -- { className: "bt-mw-state bt-mw-work" }, -- (0, import_react30.createElement)("span", null, (0, import_react30.createElement)(Shimmer, null, `${workPhrase(entry.doing.tool)}…`), where ? ` ${where}` : "", since(now, entry.doing.since)), -- entry.here ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null -- ) -- ); --} --function useMemberWork(room2, useWork) { -- const work = useWork?.((value) => value) ?? NO_WORK; -- const entries2 = room2 ? memberWork(room2, work) : []; -- const now = useNow2(entries2.some((entry) => entry.state !== "idle")); -- return { entries: entries2, now }; -+ } catch { -+ } -+ } -+ } -+ try { -+ migrateBrowsers(storage); -+ } catch { -+ } -+ try { -+ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -+ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -+ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -+ } -+ } catch { -+ } - } --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-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} --.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} --.bt-mw-row:hover{background:var(--bt-hover)} --.bt-mw-row>.bt-mark{flex:none} --.bt-mw-row-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-mw-row-time{flex:none;font-size:12px;color:var(--bt-ink-3);white-space:nowrap} --.bt-mw-work{display:flex;align-items:center;justify-content:space-between;gap:8px} --.bt-mw-stop{flex:none;height:22px;padding:0 10px;border:1px solid var(--bt-line,rgba(0,0,0,.12));border-radius:11px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer} --.bt-mw-stop:hover{background:var(--bt-hover)} --.bt-mw-stop:disabled{opacity:.5;cursor:default} --.bt-event.bt-mw-dock{margin:0 0 6px;pointer-events:none} --.bt-mw-dock>button{pointer-events:auto;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards} --.bt-mw-line{gap:6px!important} --.bt-mw-stack{display:inline-flex;align-items:center;flex:none;line-height:0} --.bt-mw-stack>*+*{margin-left:-4px} --.bt-mw-face{position:relative;display:inline-flex;flex:none;border-radius:50%} --.bt-mw-stack .bt-mw-face{box-shadow:0 0 0 1.5px var(--bt-main,#fff)} --.bt-mw-face[data-ask]::after{content:"";position:absolute;right:-2px;bottom:-2px;width:7px;height:7px;border-radius:50%;background:#e3a008;box-shadow:0 0 0 1.5px var(--bt-main,#fff)} --.bt-mw-ask{color:#b7791f;flex:none;white-space:nowrap} --body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a} --.bt-mw-member{padding:0 6px 8px 42px;display:flex;flex-direction:column;gap:6px} --.bt-mw-state{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:-4px} --.bt-mw-card{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border-radius:12px;background:var(--bt-hover);font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-mw-tool{font-weight:600;color:var(--bt-ink)} --.bt-mw-text{color:var(--bt-ink);overflow-wrap:anywhere} --.bt-mw-reason{overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} --.bt-mw-code{display:block;font:11px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--bt-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-mw-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px} --.bt-mw-actions>button{height:28px;padding:0 12px;font-size:12px} --.bt-mw-actions>button:disabled{opacity:.5;cursor:default} --@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} --@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} --`; - - // src/client/details-drawer.js --var import_react31 = require("react"); --function transcriptOf(list, id, name, hiddenTurns = []) { -- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; -- if (!Array.isArray(outline)) return ""; -- const lines = []; -- for (const entry of outline) { -- if (hiddenTurns.includes(entry.turn)) continue; -- if (entry.prompt && !HEADER.test(entry.prompt)) lines.push(`**${t("You")}:** ${entry.prompt.trim()}`); -- if (entry.response) lines.push(`**${name}:** ${stripHeader(entry.response).trim()}`); -- } -- return lines.join("\n\n"); --} -+var import_react43 = require("react"); - var TAB_LABELS = { details: "Details", browser: "Browser" }; - var hostOf = (url) => { - try { -@@ -11289,10 +11289,10 @@ var EMPTY_TABS = { ids: [], active: null }; - var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; - var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); - function PermissionRow({ bot, roster, actions, run }) { -- const [pending, setPending] = (0, import_react31.useState)(null); -+ const [pending, setPending] = (0, import_react43.useState)(null); - const catalog = roster.permissions; - const saved = bot.permission ?? catalog?.defaultPreset; -- (0, import_react31.useEffect)(() => { -+ (0, import_react43.useEffect)(() => { - if (pending === saved) setPending(null); - }, [pending, saved]); - if (!catalog?.options?.length) return null; -@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) { - setPending(value); - void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); - }; -- return (0, import_react31.createElement)( -+ return (0, import_react43.createElement)( +- 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 }) { ++ return (0, import_react37.createElement)( ++ "button", ++ { ++ type: "button", ++ className: "bt-set-row", ++ "data-workflow": item.id, ++ "data-source": source, ++ onClick: () => onOpen?.(item) ++ }, ++ (0, import_react37.createElement)( ++ "span", ++ { 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 || "") ++ ), ++ source === "own" ? (0, import_react37.createElement)( ++ "span", ++ { className: "bt-set-actions" }, ++ (0, import_react37.createElement)("button", { ++ type: "button", ++ className: "bt-set-action", ++ "aria-label": t("Rename"), ++ title: t("Rename"), ++ onClick: (event) => { ++ event.stopPropagation(); ++ onRename?.(item); ++ } ++ }, "✎"), ++ (0, import_react37.createElement)("button", { ++ type: "button", ++ className: "bt-set-action", ++ "aria-label": t("Delete"), ++ title: t("Delete"), ++ onClick: (event) => { ++ event.stopPropagation(); ++ onDelete?.(item); ++ } ++ }, "×") ++ ) : null, ++ (0, import_react37.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›") ++ ); ++} ++function alertCopy(error) { ++ if (error === WORKFLOW_PLUGIN_HINT) return t("The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."); ++ return error || t("Could not read workflows"); ++} ++function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, canvasWidth = 0, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) { ++ const graph = phase === "ready" ? openedGraph(pack, opened) : null; ++ const templates = pack?.templates ?? []; ++ const templateIds = new Set(templates.map((item) => item.id)); ++ const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); ++ const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; ++ return (0, import_react37.createElement)( "div", - { className: "bt-drawer-permission" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Permissions")), -+ (0, import_react43.createElement)( - Segmented, - { label: t("Permissions"), value: current }, -- catalog.options.map((option) => (0, import_react31.createElement)("button", { -+ catalog.options.map((option) => (0, import_react43.createElement)("button", { - key: option.value, - type: "button", - "aria-pressed": option.value === current, -@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) { - function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { - const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); - const pages = useBrowserPages((map) => map); -- (0, import_react31.useEffect)(() => { -+ (0, import_react43.useEffect)(() => { - actions.browserEnsure?.(bot.id); - }, [bot.id]); - if (roster.browser !== true || !actions.browserShow) { -- return (0, import_react31.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); -+ return (0, import_react43.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); - } - if (state.ids.length === 0) { -- return (0, import_react31.createElement)( -+ return (0, import_react43.createElement)( +- { 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", - { className: "bt-browser-empty-mark" }, -- (0, import_react31.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), -- (0, import_react31.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -- (0, import_react31.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), -- (0, import_react31.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) -+ (0, import_react43.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), -+ (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react43.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) - ); - } -- return (0, import_react31.createElement)( -+ return (0, import_react43.createElement)( - "div", - { className: "bt-bcards" }, - state.ids.map((tabId, index) => { - const page = pages[tabId] ?? {}; - const label = page.title || shortUrl(page.url) || t("New tab"); -- return (0, import_react31.createElement)( -+ return (0, import_react43.createElement)( +- { 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 ++ ) : null, ++ phase === "loading" ? (0, import_react37.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null, ++ phase === "empty" ? (0, import_react37.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null, ++ phase === "ready" && !graph ? (0, import_react37.createElement)( ++ "div", ++ { className: "bt-wf-lists" }, ++ (0, import_react37.createElement)( ++ "section", ++ { className: "bt-set-section" }, ++ (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", ++ { 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); ++ const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item; ++ return (0, import_react37.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); ++ }) ++ ) ++ ), ++ (0, import_react37.createElement)( ++ "section", ++ { className: "bt-set-section" }, ++ (0, import_react37.createElement)("h3", null, t("Yours")), ++ (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), ++ (0, import_react37.createElement)( ++ "div", ++ { className: "bt-set-card" }, ++ 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", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) ++ ) + ) +- ), +- admin === null ? null : (0, import_react23.createElement)( ++ ) : null, ++ graph ? (0, import_react37.createElement)( + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), +- (0, import_react23.createElement)( ++ { className: "bt-wf-editor" }, ++ (0, import_react37.createElement)( "div", - { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)( - "button", - { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, -- page.excerpt ? (0, import_react31.createElement)( -+ page.excerpt ? (0, import_react43.createElement)( - "span", - { className: "bt-bcard-preview" }, -- (0, import_react31.createElement)("span", { className: "bt-bcard-ptitle" }, label), -- (0, import_react31.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) -- ) : (0, import_react31.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react31.createElement)(GlobeIcon), (0, import_react31.createElement)("span", null, label)) -+ (0, import_react43.createElement)("span", { className: "bt-bcard-ptitle" }, label), -+ (0, import_react43.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) -+ ) : (0, import_react43.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react43.createElement)(GlobeIcon), (0, import_react43.createElement)("span", null, label)) - ), -- (0, import_react31.createElement)("button", { -+ (0, import_react43.createElement)("button", { - type: "button", - className: "bt-bcard-x", - "aria-label": t("Close tab"), -@@ -11367,87 +11367,195 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow - void actions.browserCloseTab(bot.id, tabId); - } - }, "×"), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)( - "div", - { className: "bt-bcard-row" }, -- (0, import_react31.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), -- (0, import_react31.createElement)("span", { className: "bt-bcard-title" }, label), -- page.url ? (0, import_react31.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null -+ (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), -+ (0, import_react43.createElement)("span", { className: "bt-bcard-title" }, label), -+ page.url ? (0, import_react43.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null - ) - ); - }), -- (0, import_react31.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react31.createElement)(PlusIcon), t("New tab")) -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react43.createElement)(PlusIcon), t("New tab")) - ); - } --function About({ text, title = "About" }) { -- const [open, setOpen] = (0, import_react31.useState)(false); -- const long = text.split("\n").length > 5 || text.length > 240; -- const body = (0, import_react31.useRef)(null); -+function About({ text: text2, title = "About" }) { -+ const [open, setOpen] = (0, import_react43.useState)(false); -+ const long = text2.split("\n").length > 5 || text2.length > 240; -+ const body = (0, import_react43.useRef)(null); - useMorph(body, open); -- return (0, import_react31.createElement)( -+ return (0, import_react43.createElement)( +- { 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, ++ opened?.source === "own" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null ++ ), ++ (0, import_react37.createElement)("h3", { className: "bt-wf-title" }, graph.name), ++ graph.description ? (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, ++ (graph.nodes ?? []).length === 0 ? (0, import_react37.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react37.createElement)(GraphCanvas, { ++ graph, ++ roster, ++ onCommit, ++ width: canvasWidth, ++ editing: opened?.source === "own" && editing === true ++ }) ++ ) : null ++ ); ++} ++function RenameModal({ workflow, onClose, onConfirm }) { ++ const [name, setName] = (0, import_react37.useState)(workflow?.name ?? ""); ++ return (0, import_react37.createElement)( + "div", -+ null, -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t(title)), -+ (0, import_react43.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), -+ long ? (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ { className: "bt-modal-mask", onClick: (event) => { ++ if (event.target === event.currentTarget) onClose?.(); ++ } }, ++ (0, import_react37.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)( ++ "label", ++ { className: "bt-field" }, ++ t("Name"), ++ (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) ++ ), ++ (0, import_react37.createElement)( ++ "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 || 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)( ++ ) + ); +} -+function SharedRoomApprovals({ room: room2 }) { -+ const [items, setItems] = (0, import_react43.useState)(room2.pendingApprovals ?? []); -+ const [busy, setBusy] = (0, import_react43.useState)(""); -+ const face = room2.origin === "face-to-face"; -+ (0, import_react43.useEffect)(() => { -+ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; -+ if (haveItems) setItems(room2.pendingApprovals); -+ if (room2.viewerRole !== "invited-owner" && !face) return void 0; -+ let alive = true; -+ call("social/status", {}).then((value) => { -+ if (!alive) return; -+ if (room2.viewerRole === "invited-owner" && !haveItems) { -+ setItems(Array.isArray(value?.approvals) ? value.approvals : []); -+ } -+ }, () => { ++function DeleteModal({ workflow, onClose, onConfirm }) { ++ return (0, import_react37.createElement)( ++ "div", ++ { className: "bt-modal-mask", onClick: (event) => { ++ if (event.target === event.currentTarget) onClose?.(); ++ } }, ++ (0, import_react37.createElement)( + "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ { className: "bt-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-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")) ++ ) + ) + ); + } +-var GROUP_SETTINGS_CSS = ` +-.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} +-.bt-ng-step{animation:bt-veil-in .22s ease-out} +-.bt-ng-label{display:inline-flex;align-items:center;gap:6px} +-.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} +-.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} +-.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-ng-chip:active{scale:.96} +-.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} +-.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} +-.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} +-.bt-gs-member{gap:8px} +-.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} +-.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} +-.bt-gs-add{gap:8px;color:var(--bt-ink-2)} +-.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} +-.bt-bs-stack .bt-seg{align-self:stretch} +-@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} +-`; ++function WorkflowsPage({ call: call4, roster }) { ++ const [phase, setPhase] = (0, import_react37.useState)("loading"); ++ const [pack, setPack] = (0, import_react37.useState)(null); ++ const [error, setError] = (0, import_react37.useState)(""); ++ const [opened, setOpened] = (0, import_react37.useState)(null); ++ const [editing, setEditing] = (0, import_react37.useState)(false); ++ const [renaming, setRenaming] = (0, import_react37.useState)(null); ++ const [deleting, setDeleting] = (0, import_react37.useState)(null); ++ const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows")); ++ const apply2 = (settled) => { ++ setPhase(settled.phase); ++ setPack(settled.pack); ++ setError(settled.error); ++ }; ++ (0, import_react37.useEffect)(() => { ++ let gone = false; ++ readWorkflows(invoke).then((settled) => { ++ if (!gone) apply2(settled); + }); + return () => { -+ alive = false; ++ gone = true; ++ }; ++ }, []); ++ const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); ++ (0, import_react37.useEffect)(() => { ++ if (!live || editing) return void 0; ++ let gone = false; ++ const timer = setInterval(() => { ++ readWorkflows(invoke).then((settled) => { ++ if (!gone) apply2(settled); ++ }); ++ }, POLL_MS6); ++ return () => { ++ gone = true; ++ clearInterval(timer); + }; -+ }, [room2.id, room2.viewerRole, room2.pendingCount, face]); -+ if (room2.viewerRole !== "invited-owner" && !face || !items.length) return null; -+ const decide = async (item, decision) => { -+ setBusy(item.id); -+ try { -+ await call("social/decide-approval", { approvalId: item.id, decision }); -+ setItems((list) => list.filter((entry) => entry.id !== item.id)); -+ } finally { -+ setBusy(""); ++ }, [live, editing]); ++ const retry = () => { ++ setPhase("loading"); ++ setError(""); ++ setOpened(null); ++ readWorkflows(invoke).then(apply2); ++ }; ++ const open = (item, source) => { ++ if (source === "factory") { ++ const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); ++ setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); ++ setEditing(false); ++ return; + } ++ setOpened({ source: "own", id: item.id }); ++ setEditing(false); + }; -+ return (0, import_react43.createElement)( ++ const useTemplate = (id) => { ++ const template = typeof id === "string" ? id : ""; ++ if (!template) return; ++ Promise.resolve(invoke("workflow/create", { template })).then((value) => { ++ const settled = settleWorkflowRead(value); ++ if (settled.phase === "error") { ++ setError(settled.error); ++ return; ++ } ++ const before = new Set((pack?.workflows ?? []).map((item) => item.id)); ++ const made = settled.pack.workflows.find((item) => !before.has(item.id)); ++ setPack(settled.pack); ++ setPhase(settled.phase); ++ setError(""); ++ if (made) { ++ setOpened({ source: "own", id: made.id }); ++ setEditing(true); ++ } ++ }).catch((failure) => { ++ setError(workflowCallError(failure, t("Could not copy this workflow"))); ++ }); ++ }; ++ const rename = (item) => { ++ setRenaming(item); ++ }; ++ const confirmRename = (workflowId, name) => { ++ if (!name.trim()) return; ++ Promise.resolve(invoke("workflow/rename", { workflowId, name: name.trim() })).then((value) => { ++ const settled = settleWorkflowRead(value); ++ if (settled.phase === "error") { ++ setError(settled.error); ++ return; ++ } ++ setPack(settled.pack); ++ setPhase("ready"); ++ setError(""); ++ setRenaming(null); ++ }).catch((failure) => { ++ setError(workflowCallError(failure, t("Could not rename this workflow"))); ++ }); ++ }; ++ const deleteWorkflow = (item) => { ++ setDeleting(item); ++ }; ++ const confirmDelete = (workflowId) => { ++ Promise.resolve(invoke("workflow/delete", { workflowId })).then((value) => { ++ const settled = settleWorkflowRead(value); ++ if (settled.phase === "error") { ++ setError(settled.error); ++ return; ++ } ++ setPack(settled.pack); ++ setPhase(settled.phase); ++ setError(""); ++ setDeleting(null); ++ if (opened?.id === workflowId) setOpened(null); ++ }).catch((failure) => { ++ setError(workflowCallError(failure, t("Could not delete this workflow"))); ++ }); ++ }; ++ const commit = (graph) => { ++ if (!opened || opened.source !== "own") return; ++ Promise.resolve(invoke("workflow/graph", { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges })).then((value) => { ++ const settled = settleWorkflowRead(value); ++ if (settled.phase === "error") { ++ setError(settled.error); ++ return; ++ } ++ setPack(settled.pack); ++ setPhase("ready"); ++ setError(""); ++ }).catch((failure) => { ++ setError(workflowCallError(failure, t("Could not save this workflow"))); ++ }); ++ }; ++ return (0, import_react37.createElement)( + "div", -+ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react43.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), -+ items.map((item) => (0, import_react43.createElement)( -+ "div", -+ { key: item.id, className: "bt-set-row" }, -+ (0, import_react43.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) -+ )) ++ null, ++ (0, import_react37.createElement)(WorkflowsView, { ++ phase, ++ pack, ++ error, ++ opened, ++ editing, ++ roster, ++ onOpen: open, ++ onBack: () => { ++ setOpened(null); ++ setEditing(false); ++ }, ++ onEditing: setEditing, ++ onRetry: retry, ++ onUse: useTemplate, ++ onCommit: commit, ++ onRename: rename, ++ onDelete: deleteWorkflow ++ }), ++ renaming ? (0, import_react37.createElement)(RenameModal, { ++ workflow: renaming, ++ onClose: () => setRenaming(null), ++ onConfirm: (name) => confirmRename(renaming.id, name) ++ }) : null, ++ deleting ? (0, import_react37.createElement)(DeleteModal, { ++ workflow: deleting, ++ onClose: () => setDeleting(null), ++ onConfirm: () => confirmDelete(deleting.id) ++ }) : null + ); +} -+function FaceMembers({ room: room2, roster, actions }) { -+ const people2 = facePeople(room2, roster); -+ const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); -+ return (0, import_react43.createElement)( - "div", - null, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null -+ (0, import_react43.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), -+ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react43.createElement)(FacePersonRow, { -+ key: person.key, -+ person, -+ roster, -+ className: "bt-member", -+ onOpenBot: (id) => actions.openSession(id), -+ lead: room2.face?.lead?.handle -+ }))) - ); + + // src/client/usage-data.js + var HOUR = 0; +@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { + } + return result; } - function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) { -+ const live = room2.presentedParticipants ?? room2.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); - const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); - const { entries: entries2, now } = useMemberWork(room2, useWork); -+ const invited = room2.viewerRole === "invited-owner"; -+ const title = roomTitle(room2); -+ const leaveMine = () => { -+ const mine = remotes.find((p) => p.ownerId === roster.self?.memberId) || members.find((member) => member.ownerId === roster.self?.memberId); -+ if (mine) void actions.updateRoom(room2.id, { removeParticipants: [{ kind: "bot", id: mine.handle || mine.id, remote: Boolean(mine.handle) }] }); -+ }; -+ const face = room2.origin === "face-to-face"; - return [ -- room2.notice ? (0, import_react31.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react31.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), -+ invited && room2.sharedFrom && !face ? (0, import_react43.createElement)( -+ "div", -+ { key: "from", className: "bt-shared-from" }, -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, title), -+ (0, import_react43.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), -+ room2.sharedFrom.invitedAt ? (0, import_react43.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) -+ ) : face ? null : room2.notice ? (0, import_react43.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react43.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), -+ face ? null : (0, import_react43.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), -+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react43.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react43.createElement)( - "div", - { key: "members" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })), -- (0, import_react31.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => { -- const entry = entries2.find((item) => item.id === member.id); -- return (0, import_react31.createElement)( -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), -+ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, [ -+ ...humans.map((person) => (0, import_react43.createElement)( - "div", -- { key: member.id, role: "listitem" }, -- (0, import_react31.createElement)( -+ { key: person.id, role: "listitem" }, -+ (0, import_react43.createElement)( - "div", - { className: "bt-member" }, -- (0, import_react31.createElement)( -- "button", -- { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, -- (0, import_react31.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), -- (0, import_react31.createElement)("span", null, member.name) -+ (0, import_react43.createElement)("span", { className: "bt-member-name" }, (0, import_react43.createElement)("span", null, person.displayName || person.id)), -+ (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Human")), -+ person.role === "admin" ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -+ ) -+ )), -+ ...remotes.map((person) => (0, import_react43.createElement)( -+ "div", -+ { key: person.handle || person.id, role: "listitem" }, -+ (0, import_react43.createElement)( -+ "div", -+ { className: "bt-member" }, -+ (0, import_react43.createElement)( -+ "span", -+ { className: "bt-member-name" }, -+ (0, import_react43.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react43.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) - ), -- room2.admin === member.id ? (0, import_react31.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -- ), -- entry ? (0, import_react31.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null -- ); -- })) -+ face ? null : (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) -+ ) -+ )), -+ ...members.map((member) => { -+ const entry = entries2.find((item) => item.id === member.id); -+ return (0, import_react43.createElement)( -+ "div", -+ { key: member.id, role: "listitem" }, -+ (0, import_react43.createElement)( -+ "div", -+ { className: "bt-member" }, -+ (0, import_react43.createElement)( -+ "button", -+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, -+ (0, import_react43.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), -+ (0, import_react43.createElement)("span", null, member.name) -+ ), -+ room2.admin === member.id ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -+ ), -+ entry ? (0, import_react43.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null -+ ); -+ }) -+ ]) - ), -- (0, import_react31.createElement)( -+ face ? null : (0, import_react43.createElement)( - "div", - { key: "mode" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Who answers")), -- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Who answers")), -+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) - ), -- usage ? (0, import_react31.createElement)( -+ face || !usage ? null : (0, import_react43.createElement)( - "div", - { key: "usage" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), -- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, room2.name), (0, import_react31.createElement)("span", null, usage)) -- ) : null -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, room2.name), (0, import_react43.createElement)("span", null, usage)) -+ ) - ]; +-function trimmed(value) { +- const text = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); +- return text.includes(".") ? text.replace(/\.?0+$/, "") : text; ++function trimmed2(value) { ++ const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); ++ return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; + } + function compact(count) { + for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { +- if (count >= size * 0.9995) return `${trimmed(count / size)}${unit}`; ++ if (count >= size * 0.9995) return `${trimmed2(count / size)}${unit}`; + } + return String(Math.round(count)); + } +@@ -9294,11 +9294,11 @@ function percent(part, whole) { + } + + // src/client/usage-page.js +-var import_react24 = require("react"); ++var import_react38 = require("react"); + var HEAT_WEEKS = 53; + var TOP_SERIES = 7; + var RING_R = 42; +-var RING = 2 * Math.PI * RING_R; ++var RING2 = 2 * Math.PI * RING_R; + var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; + var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; + var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; +@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) { + return dates({ year: "numeric", month: "long" }).format(bucket.start); } - function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { - const owner = roster.byId[sessionId]; - const part = owner ? currentPart(owner) : sessionId; - const isMainBot = isMainOf(roster, owner?.id); -- const [tab, setTab] = (0, import_react31.useState)("details"); -+ const [tab, setTab] = (0, import_react43.useState)("details"); - const room2 = roster.roomsById[sessionId]; -- const [view, setView] = (0, import_react31.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); -- const [editing, setEditing] = (0, import_react31.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); -- const [draftName, setDraftName] = (0, import_react31.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); -- const [copied, setCopied] = (0, import_react31.useState)(false); -- const [error, setError] = (0, import_react31.useState)(""); -+ const [view, setView] = (0, import_react43.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); -+ const [editing, setEditing] = (0, import_react43.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); -+ const [draftName, setDraftName] = (0, import_react43.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); -+ const [copied, setCopied] = (0, import_react43.useState)(false); -+ const [error, setError] = (0, import_react43.useState)(""); - const glide = useGlide(tab); -- const last = (0, import_react31.useRef)({ view, tab, move: void 0 }); -+ const last = (0, import_react43.useRef)({ view, tab, move: void 0 }); - if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; - else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; - const move = last.current.move; -@@ -11457,7 +11457,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a - const bot = owner; - const running = useSessionStatus((map) => map.get(part)?.running === true); - const doing = useActivity((map) => map[bot?.id ?? sessionId]); -- const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part])); -+ const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part], copyNames(roster))); - const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); - const run = (promise) => Promise.resolve(promise).then(() => { - setError(""); -@@ -11467,41 +11467,67 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a - return false; - }); - const share = () => { -- void navigator.clipboard?.writeText(transcript).then(() => { -+ const text2 = transcript; -+ if (!text2) { -+ actions.notify?.(t("Nothing to copy yet")); -+ return; -+ } -+ void copiedToClipboard(text2).then((written) => { -+ if (!written) { -+ actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); -+ return; -+ } - setCopied(true); - setTimeout(() => setCopied(false), 1600); -+ actions.notify?.(t("Conversation copied as Markdown")); - }); - }; -- const errorLine = error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -- if ((bot || room2) && view === "settings") { -+ const errorLine = error ? (0, import_react43.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -+ if ((bot || room2) && view === "settings" && !bot?.remote) { - return [ -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)( - "div", - { key: "head", className: "bt-subhead", "data-move": move }, -- (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react31.createElement)(ChevronLeftIcon)), -- (0, import_react31.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), -- hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) -+ (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react43.createElement)(ChevronLeftIcon)), -+ (0, import_react43.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), -+ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) - ), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)( - "div", - { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, - errorLine, -- bot ? (0, import_react31.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react31.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) -+ bot ? (0, import_react43.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react43.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) - ) - ]; + function useTween(value) { +- const [shown, setShown] = (0, import_react24.useState)(0); +- const from = (0, import_react24.useRef)(0); +- (0, import_react24.useEffect)(() => { ++ const [shown, setShown] = (0, import_react38.useState)(0); ++ const from = (0, import_react38.useRef)(0); ++ (0, import_react38.useEffect)(() => { + if (reducedMotion() || typeof requestAnimationFrame !== "function") { + from.current = value; + setShown(value); +@@ -9363,9 +9363,9 @@ function useTween(value) { + }, [value]); + return shown; + } +-var glyphBox = (size, icon2) => (0, import_react24.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react24.createElement)(icon2)); ++var glyphBox = (size, icon3) => (0, import_react38.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react38.createElement)(icon3)); + function ModelGlyph({ name, paint, size }) { +- return (0, import_react24.createElement)("span", { ++ return (0, import_react38.createElement)("span", { + className: "bt-us-model", + "aria-hidden": true, + style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } +@@ -9382,7 +9382,7 @@ function ownerFace(owner, roster) { + sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, + paint: paintCss(colorOf(bot)), + stops: GRADIENTS[colorOf(bot)], +- glyph: (size) => (0, import_react24.createElement)(BotAvatar, { bot, size, badge: false, live: false }) ++ glyph: (size) => (0, import_react38.createElement)(BotAvatar, { bot, size, badge: false, live: false }) + }; } -- const top = (0, import_react31.createElement)( -+ const top = (0, import_react43.createElement)( + if (owner.kind === "room") { +@@ -9392,7 +9392,7 @@ function ownerFace(owner, roster) { + name: owner.name || t("Deleted group chat"), + sub: owner.deleted ? t("Deleted") : t("Group chat"), + paint: "var(--bt-us-rest)", +- glyph: (size) => room2 ? (0, import_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) ++ glyph: (size) => room2 ? (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) + }; + } + if (owner.kind === "agent") { +@@ -9411,33 +9411,33 @@ function kindLabel(kind, botFocus) { + return labels[kind] ?? kind; + } + function Tabs({ label, value, options, onChange, boxRef }) { +- const names = options.map(([, text]) => text).join("\n"); ++ const names = options.map(([, text2]) => text2).join("\n"); + const glide = useGlide(`${value} + ${names}`, { box: boxRef }); +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( "div", - { className: "bt-drawer-top", "data-move": side }, -- bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? t("Edit Bot") : t("Group settings"), title: bot ? t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => setView("settings") }, (0, import_react31.createElement)(GearIcon)) : null, -- bot && roster.browser === true && actions.openBrowser ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react31.createElement)(GlobeIcon)) : null, -- bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react31.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react31.createElement)(ShareIcon)) : null, -- hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) -+ bot || room2 ? (0, import_react43.createElement)("button", { -+ type: "button", -+ className: "bt-round", -+ "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -+ title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -+ "data-info-row": "settings", -+ onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") -+ }, (0, import_react43.createElement)(GearIcon)) : null, -+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react43.createElement)(GlobeIcon)) : null, -+ bot || room2 ? (0, import_react43.createElement)("button", { -+ type: "button", -+ className: "bt-round", -+ // #1347: the accessible name says what the click does, which is copy — "Share conversation" -+ // promised a sharing step that never came. It still says "Copied" after a copy, so a screen -+ // reader hears the same confirmation the toast gives. -+ "aria-label": copied ? t("Copied conversation") : t("Copy conversation"), -+ title: copied ? t("Copied") : t("Copy conversation as Markdown"), -+ onClick: share -+ }, copied ? (0, import_react43.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react43.createElement)(ShareIcon)) : null, -+ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) + { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, +- (0, import_react24.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- options.map(([id, text, icon2]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon2 ? (0, import_react24.createElement)(icon2) : null, text)) ++ (0, import_react38.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ options.map(([id, text2, icon3]) => (0, import_react38.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react38.createElement)(icon3) : null, text2)) ); - if (!bot && !room2) { - return [ -- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), -- (0, import_react31.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react31.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) -+ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top), -+ (0, import_react43.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react43.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) - ]; - } - const current = bot?.name ?? room2.name; -@@ -11515,12 +11515,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a - const subtitle = bot ? bot.role : roomSubtitle(room2, roster); - const tabs = bot ? ["details", "browser"] : ["details"]; - return [ -- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top), -+ (0, import_react43.createElement)( + } + function Stats({ range, total, previous }) { + const shown = useTween(total.total); + const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; +- const cell = (label, value) => (0, import_react24.createElement)("div", { className: "bt-us-stat" }, (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react24.createElement)("span", { className: "bt-us-stat-value" }, value)); +- return (0, import_react24.createElement)( ++ const cell = (label, value) => (0, import_react38.createElement)("div", { className: "bt-us-stat" }, (0, import_react38.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react38.createElement)("span", { className: "bt-us-stat-value" }, value)); ++ return (0, import_react38.createElement)( + "section", + { className: "bt-us-stats", style: { "--i": 0 } }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( "div", - { key: "id", className: "bt-drawer-id", "data-move": side }, -- bot ? (0, import_react31.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -- editing ? (0, import_react31.createElement)("input", { -+ bot ? (0, import_react43.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react43.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ bot?.remote ? (0, import_react43.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react43.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react43.createElement)("input", { - className: "bt-drawer-name-input", - autoFocus: true, - value: draftName, -@@ -11535,98 +11535,97 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a - setEditing(false); - } - } -- }) : (0, import_react31.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { -+ }) : (0, import_react43.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { - setDraftName(room2 && !room2.named ? "" : current); - setEditing(true); - } }, current), -- subtitle ? (0, import_react31.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null -+ subtitle ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null - ), -- (0, import_react31.createElement)( -+ bot?.remote ? null : (0, import_react43.createElement)( + { className: "bt-us-stat bt-us-stat-main" }, +- (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), ++ (0, import_react38.createElement)( + "span", + { className: "bt-us-stat-value" }, + compact(shown), +- (0, import_react24.createElement)("small", null, t("tokens")), +- change === null ? null : (0, import_react24.createElement)( ++ (0, import_react38.createElement)("small", null, t("tokens")), ++ change === null ? null : (0, import_react38.createElement)( + "span", + { className: "bt-us-delta", title: compareText(range) }, + `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` +@@ -9455,83 +9455,83 @@ function niceTop(max) { + return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); + } + function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { +- const [hover, setHover] = (0, import_react24.useState)(null); ++ const [hover, setHover] = (0, import_react38.useState)(null); + const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); + const count = columns.length; + const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); + const tipOf = new Map(tipSeries.map((entry) => [entry.key, entry])); + const tip = hover === null ? null : columns[hover]; + const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + "section", + { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( "div", - { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, -- (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- tabs.map((key) => (0, import_react31.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) -+ (0, import_react43.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ tabs.map((key) => (0, import_react43.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) + { className: "bt-us-card-head" }, +- (0, import_react24.createElement)("h3", null, t("Tokens over time"), (0, import_react24.createElement)("span", null, unitLabel(range.unit))), +- onBy ? (0, import_react24.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null ++ (0, import_react38.createElement)("h3", null, t("Tokens over time"), (0, import_react38.createElement)("span", null, unitLabel(range.unit))), ++ onBy ? (0, import_react38.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null ), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)( +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( "div", - { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, - errorLine, -- room2 && tab === "details" ? (0, import_react31.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, -- bot && tab === "details" ? [ -- (0, import_react31.createElement)( -+ room2 && tab === "details" ? (0, import_react43.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, -+ bot && bot.remote && tab === "details" ? [ -+ bot.remote.handle ? (0, import_react43.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, -+ (0, import_react43.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) -+ ] : null, -+ bot && !bot.remote && tab === "details" ? [ -+ (0, import_react43.createElement)( - "div", - { key: "model", className: "bt-drawer-model" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Model")), -- (0, import_react31.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), -- modelOf(bot, roster) ? null : (0, import_react31.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Model")), -+ (0, import_react43.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), -+ modelOf(bot, roster) ? null : (0, import_react43.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) - ), -- (0, import_react31.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), -- (0, import_react31.createElement)( -+ (0, import_react43.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), -+ (0, import_react43.createElement)( + { className: "bt-us-chart-body" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, +- [1, 0.5].map((level) => (0, import_react24.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react24.createElement)("span", null, compact(top * level)))), +- (0, import_react24.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react24.createElement)("span", null, "0")), +- (0, import_react24.createElement)( ++ [1, 0.5].map((level) => (0, import_react38.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react38.createElement)("span", null, compact(top * level)))), ++ (0, import_react38.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react38.createElement)("span", null, "0")), ++ (0, import_react38.createElement)( "div", - { key: "folder", className: "bt-drawer-model" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Working folder")), -- (0, import_react31.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Working folder")), -+ (0, import_react43.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) + { className: "bt-us-cols" }, +- columns.map((column, at) => (0, import_react24.createElement)("div", { ++ columns.map((column, at) => (0, import_react38.createElement)("div", { + key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, + className: "bt-us-col", + "data-on": hover === at ? "" : void 0, + onMouseEnter: () => setHover(at) +- }, column.total > 0 ? (0, import_react24.createElement)( ++ }, column.total > 0 ? (0, import_react38.createElement)( + "div", + { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, +- column.parts.map(([key, value]) => value > 0 ? (0, import_react24.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) +- ) : (0, import_react24.createElement)("div", { className: "bt-us-stub" }))) ++ column.parts.map(([key, value]) => value > 0 ? (0, import_react38.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) ++ ) : (0, import_react38.createElement)("div", { className: "bt-us-stub" }))) ), -- bot.instructions ? (0, import_react31.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react31.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), -- roster.memory === false ? null : (0, import_react31.createElement)(MemoryRow, { key: "memory", bot, actions }), -- usage ? (0, import_react31.createElement)( -+ bot.instructions ? (0, import_react43.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react43.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), -+ roster.memory === false ? null : (0, import_react43.createElement)(MemoryRow, { key: "memory", bot, actions }), -+ usage ? (0, import_react43.createElement)( +- tip ? (0, import_react24.createElement)( ++ tip ? (0, import_react38.createElement)( "div", - { key: "usage" }, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), -- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, bot.name), (0, import_react31.createElement)("span", null, usage)) -+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, bot.name), (0, import_react43.createElement)("span", null, usage)) - ) : null, -- (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions }) -+ (0, import_react43.createElement)(ScheduleSummary, { key: "routines", bot, actions }) - ] : null, -- bot && tab === "browser" ? (0, import_react31.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null -+ bot && !bot.remote && tab === "browser" ? (0, import_react43.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null - ) - ]; - } - - // src/client/browser-pane.js --var import_react32 = require("react"); --var import_react_dom8 = require("react-dom"); -+var import_react44 = require("react"); -+var import_react_dom10 = require("react-dom"); - var PANEL_LINGER_MS = 300; - var WIDTH_KEY = STORAGE.browserWidth; --var DEFAULT_WIDTH = 520; --var DETAILS_WIDTH = 320; --var DETAILS_MAX = 520; --var MIN_WIDTH = 360; --var MIN_CHAT = 420; - var READ_FLASH_MS = 2400; - var EMPTY_PAGE = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, excerpt: "" }; - var EMPTY_TABS2 = { ids: [], active: null }; - var SPRING_LEAD2 = springEasing(576, 39.4); - var SPRING_TRAIL2 = springEasing(144, 19.7); --var icon = (paths) => function BrowserIcon() { -- return (0, import_react32.createElement)( -+var icon2 = (paths) => function BrowserIcon() { -+ return (0, import_react44.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 }, -- paths.map((d, index) => (0, import_react32.createElement)("path", { key: index, d })) -+ paths.map((d, index) => (0, import_react44.createElement)("path", { key: index, d })) + { className: "bt-us-tip", style: tipSide }, +- (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, tip.title), +- (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), +- tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-tip-title" }, tip.title), ++ (0, import_react38.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), ++ tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react38.createElement)( + "div", + { key, className: "bt-us-tip-row" }, +- (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), +- (0, import_react24.createElement)("span", null, tipOf.get(key)?.name), +- (0, import_react24.createElement)("b", null, compact(value)) ++ (0, import_react38.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), ++ (0, import_react38.createElement)("span", null, tipOf.get(key)?.name), ++ (0, import_react38.createElement)("b", null, compact(value)) + )) + ) : null + ), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-ticks", "aria-hidden": true }, + // A preset counts its labels back from now; a picked span forward from its first day. +- columns.map((column, at) => (0, import_react24.createElement)( ++ columns.map((column, at) => (0, import_react38.createElement)( + "span", + { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, + (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" + )) + ) + ), +- series.length > 1 || series[0]?.pick ? (0, import_react24.createElement)( ++ series.length > 1 || series[0]?.pick ? (0, import_react38.createElement)( + "div", + { className: "bt-us-legend" }, +- series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", { ++ series.map((entry) => (0, import_react38.createElement)(entry.pick ? "button" : "span", { + key: entry.key, + className: "bt-us-key", + ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} +- }, (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) ++ }, (0, import_react38.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) + ) : null ); - }; --var GlobeIcon = icon(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); --var BackIcon = icon(["M12.5 4.5L7 10l5.5 5.5"]); --var ForwardIcon = icon(["M7.5 4.5L13 10l-5.5 5.5"]); --var ReloadIcon = icon(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); --var StopIcon = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); --var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); --var PlusTabIcon = icon(["M10 5v10M5 10h10"]); --var LockIcon2 = icon(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); -+var GlobeIcon = icon2(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); -+var BackIcon = icon2(["M12.5 4.5L7 10l5.5 5.5"]); -+var ForwardIcon = icon2(["M7.5 4.5L13 10l-5.5 5.5"]); -+var ReloadIcon = icon2(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); -+var StopIcon = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); -+var CloseIcon2 = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); -+var PlusTabIcon = icon2(["M10 5v10M5 10h10"]); -+var LockIcon2 = icon2(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); - function Favicon({ src, className }) { -- const [failed, setFailed] = (0, import_react32.useState)(null); -- if (!src || failed === src) return (0, import_react32.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react32.createElement)(GlobeIcon)); -- return (0, import_react32.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); -+ const [failed, setFailed] = (0, import_react44.useState)(null); -+ if (!src || failed === src) return (0, import_react44.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react44.createElement)(GlobeIcon)); -+ return (0, import_react44.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); } - var savedWidth = () => { - try { - const value = Number(localStorage.getItem(WIDTH_KEY)); -- return Number.isFinite(value) && value >= DETAILS_WIDTH ? value : null; -+ return Number.isFinite(value) && value >= MIN_DETAILS ? value : null; - } catch { - return null; - } - }; --var widthRange = (view, viewport) => view === "browser" ? [MIN_WIDTH, viewport - MIN_CHAT] : [DETAILS_WIDTH, Math.min(DETAILS_MAX, viewport - MIN_CHAT)]; - var fitWidth = (width, [low, high]) => Math.round(Math.max(low, Math.min(width, high))); -+var widthRange = (view, viewport) => panelWidthRange(view, viewport); - var room = { rule: null, selector: "" }; - var roomRule = () => { - const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -@@ -11674,11 +11674,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref - var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); - function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { - const pages = useBrowserPages((map) => map); -- const wrap = (0, import_react32.useRef)(null); -- const ind = (0, import_react32.useRef)(null); -- const spot = (0, import_react32.useRef)(null); -- const under = (0, import_react32.useRef)(null); -- (0, import_react32.useLayoutEffect)(() => { -+ const wrap = (0, import_react44.useRef)(null); -+ const ind = (0, import_react44.useRef)(null); -+ const spot = (0, import_react44.useRef)(null); -+ const under = (0, import_react44.useRef)(null); -+ (0, import_react44.useLayoutEffect)(() => { - const box = wrap.current; - const capsule = ind.current; - if (!box || !capsule) return void 0; -@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, - trail.cancel(); - }; - }); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - const box = wrap.current; - if (!box) return void 0; - const observer = new ResizeObserver(() => { -@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, - observer.observe(box); - return () => observer.disconnect(); - }, []); -- return (0, import_react32.createElement)( -+ return (0, import_react44.createElement)( - "div", - { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, -- (0, import_react32.createElement)( -+ (0, import_react44.createElement)( - "div", - { className: "bt-ptabs-in", ref: wrap }, -- (0, import_react32.createElement)( -+ (0, import_react44.createElement)( - "button", - { - type: "button", -@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, - title: bot.name, - onClick: () => onView("details") - }, -- (0, import_react32.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), -- view === "details" ? (0, import_react32.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null -+ (0, import_react44.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), -+ view === "details" ? (0, import_react44.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null +-function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick }) { ++function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick }) { + const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + onClick ? "button" : "div", + { + className: "bt-us-row", +@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick + title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), + ...onClick ? { type: "button", onClick } : {} + }, +- glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, +- (0, import_react24.createElement)( ++ glyph ? (0, import_react38.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, ++ (0, import_react38.createElement)( + "span", + { className: "bt-us-row-body" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "span", + { className: "bt-us-row-line" }, +- (0, import_react24.createElement)("span", { className: "bt-us-row-name" }, name), +- sub ? (0, import_react24.createElement)("span", { className: "bt-us-row-sub" }, sub) : null ++ (0, import_react38.createElement)("span", { className: "bt-us-row-name" }, name), ++ sub ? (0, import_react38.createElement)("span", { className: "bt-us-row-sub" }, sub) : null ), - ids.map((tabId) => { - const page = pages[tabId] ?? EMPTY_PAGE; - const on = view === "browser" && tabId === active; -- return (0, import_react32.createElement)( -+ return (0, import_react44.createElement)( - "button", - { - key: tabId, -@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, - } - } - }, -- (0, import_react32.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), -- (0, import_react32.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), -- (0, import_react32.createElement)("span", { -+ (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), -+ (0, import_react44.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), -+ (0, import_react44.createElement)("span", { - className: "bt-ptab-x", - role: "button", - "aria-label": t("Close tab"), -@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, - }, "×") - ); - }), -- (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) -+ (0, import_react44.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) +- (0, import_react24.createElement)("span", { className: "bt-us-track" }, (0, import_react24.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) ++ (0, import_react38.createElement)("span", { className: "bt-us-track" }, (0, import_react38.createElement)("span", { style: { width: `${width}%`, background: fill3 } })) ), - // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. -- (0, import_react32.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react32.createElement)(PlusTabIcon)), -- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react32.createElement)(CloseIcon2)) -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react44.createElement)(PlusTabIcon)), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react44.createElement)(CloseIcon2)) +- (0, import_react24.createElement)("span", { className: "bt-us-row-num" }, (0, import_react24.createElement)("b", null, compact(sum.total)), (0, import_react24.createElement)("small", null, `${percent(sum.total, whole)}%`)) ++ (0, import_react38.createElement)("span", { className: "bt-us-row-num" }, (0, import_react38.createElement)("b", null, compact(sum.total)), (0, import_react38.createElement)("small", null, `${percent(sum.total, whole)}%`)) ); } - function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { - const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE; -- const frame = (0, import_react32.useRef)(null); -- const input = (0, import_react32.useRef)(null); -- const [draft, setDraft] = (0, import_react32.useState)(null); -+ const frame = (0, import_react44.useRef)(null); -+ const input = (0, import_react44.useRef)(null); -+ const [draft, setDraft] = (0, import_react44.useState)(null); - const showGuest = landed && !leaving && Boolean(page.url) && !page.error; -- (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react44.useLayoutEffect)(() => { - const element = frame.current; - if (!element || leaving || !tabId) { - void actions.browserShow(null); -@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) - window.removeEventListener("resize", schedule); + function ListCard({ title, count, index, children }) { +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + "section", + { className: "bt-us-card", style: { "--i": index } }, +- (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, title, count === void 0 ? null : (0, import_react24.createElement)("span", null, count))), +- (0, import_react24.createElement)("div", { className: "bt-us-rows" }, children) ++ (0, import_react38.createElement)("div", { className: "bt-us-card-head" }, (0, import_react38.createElement)("h3", null, title, count === void 0 ? null : (0, import_react38.createElement)("span", null, count))), ++ (0, import_react38.createElement)("div", { className: "bt-us-rows" }, children) + ); + } + function Profile({ face, share, range }) { +- const arc = Math.max(0, Math.min(1, share)) * RING; +- return (0, import_react24.createElement)( ++ const arc = Math.max(0, Math.min(1, share)) * RING2; ++ return (0, import_react38.createElement)( + "section", + { className: "bt-us-profile" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "span", + { className: "bt-us-ring" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "svg", + { viewBox: "0 0 96 96", "aria-hidden": true }, +- face.stops ? (0, import_react24.createElement)("defs", null, (0, import_react24.createElement)( ++ face.stops ? (0, import_react38.createElement)("defs", null, (0, import_react38.createElement)( + "linearGradient", + { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, +- face.stops.map(([offset, color]) => (0, import_react24.createElement)("stop", { key: offset, offset, stopColor: color })) ++ face.stops.map(([offset, color]) => (0, import_react38.createElement)("stop", { key: offset, offset, stopColor: color })) + )) : null, +- (0, import_react24.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), +- arc > 0 ? (0, import_react24.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING}` } }) : null ++ (0, import_react38.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), ++ arc > 0 ? (0, import_react38.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null + ), +- (0, import_react24.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) ++ (0, import_react38.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) + ), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-profile-copy" }, +- (0, import_react24.createElement)("h3", null, face.name), +- face.sub ? (0, import_react24.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, +- (0, import_react24.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) ++ (0, import_react38.createElement)("h3", null, face.name), ++ face.sub ? (0, import_react38.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, ++ (0, import_react38.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) + ) + ); + } + var monthOf = (ms) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth(), 1); + var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); + function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +- const box = (0, import_react24.useRef)(null); ++ const box = (0, import_react38.useRef)(null); + const todayMs = hourMs(today); + const thisMonth = monthOf(todayMs); +- const [right, setRight] = (0, import_react24.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); +- const [anchor, setAnchor] = (0, import_react24.useState)(null); +- const [hover, setHover] = (0, import_react24.useState)(null); +- (0, import_react24.useEffect)(() => { ++ const [right, setRight] = (0, import_react38.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); ++ const [anchor, setAnchor] = (0, import_react38.useState)(null); ++ const [hover, setHover] = (0, import_react38.useState)(null); ++ (0, import_react38.useEffect)(() => { + const onDown = (event) => { + if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; - }, [tabId, showGuest, leaving]); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - setDraft(null); - if (landed && !page.url) input.current?.focus(); - }, [tabId, landed]); - const go = (event) => { - event.preventDefault(); -- const text = draft ?? ""; -- if (text.trim() === "") return; -+ const text2 = draft ?? ""; -+ if (text2.trim() === "") return; - setDraft(null); - input.current?.blur(); -- void actions.browserNavigate(tabId, text).then((opened) => { -+ void actions.browserNavigate(tabId, text2).then((opened) => { - if (opened) actions.browserFocus(tabId); - }); +@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { + const [from, to] = [anchor, key].sort(); + onPick(from, to); }; - const editing = draft !== null; - const secure = /^https:/i.test(page.url); -- return (0, import_react32.createElement)( -+ return (0, import_react44.createElement)( +- const weekdays = (0, import_react24.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); ++ const weekdays = (0, import_react38.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); + const month = (start, side) => { + const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); + const lead = (new Date(start).getUTCDay() + 6) % 7; +- const cells = Array.from({ length: lead }, (_, at) => (0, import_react24.createElement)("span", { key: `b${at}` })); ++ const cells = Array.from({ length: lead }, (_, at) => (0, import_react38.createElement)("span", { key: `b${at}` })); + for (let day = 1; day <= length; day += 1) { + const ms = start + (day - 1) * DAY_MS; + const key = dayKey(ms); + const weekday = (lead + day - 1) % 7; + const flag = (on) => on ? "" : void 0; +- cells.push((0, import_react24.createElement)("button", { ++ cells.push((0, import_react38.createElement)("button", { + key, + type: "button", + className: "bt-us-day", +@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { + "data-has": flag(active.has(key)), + onMouseEnter: () => setHover(key), + onClick: () => press(key) +- }, (0, import_react24.createElement)("b", null, day))); ++ }, (0, import_react38.createElement)("b", null, day))); + } +- const nav = side === "left" ? (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react24.createElement)(ChevronLeftIcon)) : (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react24.createElement)(ChevronRightIcon)); +- return (0, import_react24.createElement)( ++ const nav = side === "left" ? (0, import_react38.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react38.createElement)(ChevronLeftIcon)) : (0, import_react38.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react38.createElement)(ChevronRightIcon)); ++ return (0, import_react38.createElement)( + "div", + { key: start, className: "bt-us-month" }, +- (0, import_react24.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react24.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), +- (0, import_react24.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react24.createElement)("span", { key: at }, name))), +- (0, import_react24.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) ++ (0, import_react38.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react38.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), ++ (0, import_react38.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react38.createElement)("span", { key: at }, name))), ++ (0, import_react38.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) + ); + }; + const shortcuts = [ +@@ -9671,30 +9671,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { + ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] + ]; + const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( "div", - { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, -- (0, import_react32.createElement)( -+ (0, import_react44.createElement)( + { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( "div", - { className: "bt-browser-bar" }, -- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react32.createElement)(BackIcon)), -- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react32.createElement)(ForwardIcon)), -- (0, import_react32.createElement)("button", { -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react44.createElement)(BackIcon)), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react44.createElement)(ForwardIcon)), -+ (0, import_react44.createElement)("button", { + { className: "bt-us-pick-quick" }, +- shortcuts.map(([id, label, from, to]) => (0, import_react24.createElement)("button", { ++ shortcuts.map(([id, label, from, to]) => (0, import_react38.createElement)("button", { + key: id, type: "button", - className: "bt-icon-btn", - disabled: !page.url, - "aria-label": page.loading ? t("Stop") : t("Reload"), - title: page.loading ? t("Stop") : t("Reload"), - onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") -- }, (0, import_react32.createElement)(page.loading ? StopIcon : ReloadIcon)), -- (0, import_react32.createElement)( -+ }, (0, import_react44.createElement)(page.loading ? StopIcon : ReloadIcon)), -+ (0, import_react44.createElement)( - "form", - { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, -- (0, import_react32.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react32.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), -- (0, import_react32.createElement)("input", { -+ (0, import_react44.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react44.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), -+ (0, import_react44.createElement)("input", { - ref: input, - type: "text", - spellCheck: false, -@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) - } - }) - ), -- page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null -+ page.loading ? (0, import_react44.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null + "aria-pressed": span?.from === from && span?.to === to, + onClick: () => onPick(from, to) + }, label)) ), -- (0, import_react32.createElement)( -+ (0, import_react44.createElement)( +- (0, import_react24.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), ++ (0, import_react38.createElement)( "div", - { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, -- page.error ? (0, import_react32.createElement)( -+ page.error ? (0, import_react44.createElement)( - "div", - { className: "bt-browser-empty" }, -- (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), -- (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), -- page.url ? (0, import_react32.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { -+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), -+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), -+ page.url ? (0, import_react44.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { - void actions.browserNavigate(tabId, page.url); - } }, t("Try again")) : null -- ) : !page.url ? (0, import_react32.createElement)( -+ ) : !page.url ? (0, import_react44.createElement)( + { className: "bt-us-pick-foot", role: "status" }, +- (0, import_react24.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), +- low === null ? null : (0, import_react24.createElement)("span", null, (0, import_react24.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) ++ (0, import_react38.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), ++ low === null ? null : (0, import_react38.createElement)("span", null, (0, import_react38.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) + ) + ); + } + function Heat({ view, keep, paint, index }) { +- const [hover, setHover] = (0, import_react24.useState)(null); ++ const [hover, setHover] = (0, import_react38.useState)(null); + const today = hourMs(`${view.now.slice(0, 10)} 00`); + const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; + const from = dayKey(first); +@@ -9710,13 +9710,13 @@ function Heat({ view, keep, paint, index }) { + for (let week = 0; week < HEAT_WEEKS; week += 1) { + const monday = first + week * 7 * DAY_MS; + if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { +- months.push((0, import_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); ++ months.push((0, import_react38.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); + } + for (let day = 0; day < 7; day += 1) { + const ms = monday + day * DAY_MS; + const value = daily.get(dayKey(ms)) ?? 0; + const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; +- cells.push((0, import_react24.createElement)("span", { ++ cells.push((0, import_react38.createElement)("span", { + key: ms, + className: "bt-us-cell", + style: { "--w": week }, +@@ -9724,7 +9724,7 @@ function Heat({ view, keep, paint, index }) { + "data-future": ms > today ? "" : void 0, + "data-today": ms === today ? "" : void 0, + onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) +- }, level > 0 ? (0, import_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); ++ }, level > 0 ? (0, import_react38.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); + } + } + const tipSide = hover ? { +@@ -9733,61 +9733,61 @@ function Heat({ view, keep, paint, index }) { + } : null; + const active = [...daily.values()].filter((value) => value > 0).length; + const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + "section", + { className: "bt-us-card bt-us-heat", style: { "--i": index } }, +- (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, t("Activity")), (0, import_react24.createElement)("span", { className: "bt-us-head-note" }, summary)), +- (0, import_react24.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-card-head" }, (0, import_react38.createElement)("h3", null, t("Activity")), (0, import_react38.createElement)("span", { className: "bt-us-head-note" }, summary)), ++ (0, import_react38.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, +- (0, import_react24.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), +- hover ? (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), ++ hover ? (0, import_react38.createElement)( "div", - { className: "bt-browser-empty" }, -- (0, import_react32.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), -- (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -- (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) -+ (0, import_react44.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), -+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) + { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, +- (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), +- (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) ++ (0, import_react38.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), ++ (0, import_react38.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) ) : null + ), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-heat-scale", "aria-hidden": true }, + t("Light"), +- levels.map((level) => (0, import_react24.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react24.createElement)("i", { style: { background: soften(paint, level) } }))), ++ levels.map((level) => (0, import_react38.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react38.createElement)("i", { style: { background: soften(paint, level) } }))), + t("Heavy") ) ); -@@ -11907,41 +11907,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu - const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); - const active = tabState.active; - const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); -- const root = (0, import_react32.useRef)(null); -- const fly = (0, import_react32.useRef)(null); -- const [landed, setLanded] = (0, import_react32.useState)(false); -- const [settling, setSettling] = (0, import_react32.useState)(false); -- const [width, setWidth] = (0, import_react32.useState)(savedWidth); -- const [viewport, setViewport] = (0, import_react32.useState)(() => window.innerWidth); -- const [dragging, setDragging] = (0, import_react32.useState)(false); -- const [resizing, setResizing] = (0, import_react32.useState)(false); -+ const root = (0, import_react44.useRef)(null); -+ const fly = (0, import_react44.useRef)(null); -+ const [landed, setLanded] = (0, import_react44.useState)(false); -+ const [settling, setSettling] = (0, import_react44.useState)(false); -+ const [width, setWidth] = (0, import_react44.useState)(savedWidth); -+ const [viewport, setViewport] = (0, import_react44.useState)(() => window.innerWidth); -+ const [dragging, setDragging] = (0, import_react44.useState)(false); -+ const [resizing, setResizing] = (0, import_react44.useState)(false); - const instant = dragging || resizing; -- const [reading, setReading] = (0, import_react32.useState)(false); -+ const [reading, setReading] = (0, import_react44.useState)(false); - const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); -- (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react44.useLayoutEffect)(() => { - if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); - }, [view, width]); -- (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react44.useLayoutEffect)(() => { - chatRoom.hold(instant); - }, [instant]); -- const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth }); -+ const [last, setLast] = (0, import_react44.useState)({ view, width: panelWidth }); - if (last.view !== view || last.width !== panelWidth) { - setLast({ view, width: panelWidth }); - if (last.view !== view) setSettling(last.width !== panelWidth && !instant); - } -- const shownView = (0, import_react32.useRef)(view); -- (0, import_react32.useLayoutEffect)(() => { -+ const shownView = (0, import_react44.useRef)(view); -+ (0, import_react44.useLayoutEffect)(() => { - const switched = settling && shownView.current !== view; - shownView.current = view; - if (switched) chatRoom.hold(true); - chatRoom.set(leaving ? null : panelWidth); - if (switched) chatRoom.hold(false); - }, [view, panelWidth, leaving]); -- (0, import_react32.useEffect)(() => () => { -+ (0, import_react44.useEffect)(() => () => { - chatRoom.set(null); - chatRoom.hold(false); - }, []); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { + } + function Skeleton2() { +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + "div", + { className: "bt-us", "aria-busy": true }, +- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-sheet bt-us-skel-sheet" }, +- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), +- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), ++ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-pair" }, +- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), +- (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) ++ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), ++ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) + ) + ) + ); + } + var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null; + function UsagePage({ roster, actions, focusId }) { +- const [view, setView] = (0, import_react24.useState)(null); +- const [error, setError] = (0, import_react24.useState)(""); +- const [rangeId, setRangeId] = (0, import_react24.useState)("7d"); +- const [span, setSpan] = (0, import_react24.useState)(null); +- const [picking, setPicking] = (0, import_react24.useState)(false); +- const [by, setBy] = (0, import_react24.useState)("bot"); +- const tabs = (0, import_react24.useRef)(null); ++ const [view, setView] = (0, import_react38.useState)(null); ++ const [error, setError] = (0, import_react38.useState)(""); ++ const [rangeId, setRangeId] = (0, import_react38.useState)("7d"); ++ const [span, setSpan] = (0, import_react38.useState)(null); ++ const [picking, setPicking] = (0, import_react38.useState)(false); ++ const [by, setBy] = (0, import_react38.useState)("bot"); ++ const tabs = (0, import_react38.useRef)(null); + const focus = focusOf(focusId); +- (0, import_react24.useEffect)(() => { ++ (0, import_react38.useEffect)(() => { + let stopped = false; let timer; - const onResize = () => { - setResizing(true); -@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu - window.removeEventListener("resize", onResize); - }; - }, []); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - if (!settling) return void 0; - const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); - return () => clearTimeout(timer); - }, [settling, view]); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - const timer = setTimeout(() => setLanded(true), 360); - return () => clearTimeout(timer); - }, []); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - if (canBrowse) actions.browserEnsure(bot.id); - }, [bot?.id, canBrowse]); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); - }, [view, active, tabState.ids.length]); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - if (view !== "browser" || leaving) void actions.browserShow?.(null); - }, [view, leaving]); -- (0, import_react32.useEffect)(() => () => { -+ (0, import_react44.useEffect)(() => () => { - void actions.browserShow?.(null); + const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { +@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) { + const value = await actions.usage(); + await resized; + if (stopped) return; +- (0, import_react24.startTransition)(() => setView(value)); ++ (0, import_react38.startTransition)(() => setView(value)); + setError(""); + timer = setTimeout(load, value.scanning ? 3e3 : 2e4); + } catch (failure) { +@@ -9814,7 +9814,7 @@ function UsagePage({ roster, actions, focusId }) { }, []); -- (0, import_react32.useEffect)(() => { -+ (0, import_react44.useEffect)(() => { - if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; - setReading(true); - const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu - return actions[name](...args); - }; + const today = view?.now.slice(0, 10); + const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); +- const shaped = (0, import_react24.useMemo)(() => { ++ const shaped = (0, import_react38.useMemo)(() => { + if (!view) return null; + const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); + const lifetime = /* @__PURE__ */ new Map(); +@@ -9823,7 +9823,7 @@ function UsagePage({ roster, actions, focusId }) { + const models2 = view.models.map((model, at) => { + const name = model.name || model.model || t("Unknown model"); + const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; +- return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react24.createElement)(ModelGlyph, { name, paint, size }) }; ++ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react38.createElement)(ModelGlyph, { name, paint, size }) }; + }); + const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; + const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; +@@ -9871,23 +9871,23 @@ function UsagePage({ roster, actions, focusId }) { + }); + return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; + }, [view, rangeId, span, by, focusId, roster]); +- const root = (0, import_react24.useRef)(null); +- (0, import_react24.useEffect)(() => { ++ const root = (0, import_react38.useRef)(null); ++ (0, import_react38.useEffect)(() => { + root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); + }, [focusId]); + if (!view) { +- return error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react24.createElement)(Skeleton2); ++ return error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react38.createElement)(Skeleton2); } -- (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react44.useLayoutEffect)(() => { - const move = fly.current; - if (!move) return; - fly.current = null; -@@ -12096,7 +12096,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu - for (const name of ends) grip.addEventListener(name, onEnd); - }; - const hasStrip = canBrowse && tabState.ids.length > 0; -- return (0, import_react32.createElement)( -+ return (0, import_react44.createElement)( - "aside", - { - ref: root, -@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu - if (event.target === event.currentTarget) setLanded(true); - } - }, -- leaving ? null : (0, import_react32.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), -+ leaving ? null : (0, import_react44.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), - // Over the whole window while dragging: the cursor stays a resize cursor and the - // pages underneath (the chat, the guest) see no pointer. -- dragging ? (0, import_react_dom8.createPortal)((0, import_react32.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, -- hasStrip ? (0, import_react32.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, -- view === "browser" && bot ? (0, import_react32.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react32.createElement)( -+ dragging ? (0, import_react_dom10.createPortal)((0, import_react44.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, -+ hasStrip ? (0, import_react44.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, -+ view === "browser" && bot ? (0, import_react44.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react44.createElement)( + if (view.rows.length === 0) { +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( "div", - { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, -- (0, import_react32.createElement)(DetailsDrawer, { -+ (0, import_react44.createElement)(DetailsDrawer, { - sessionId: pane.id, - startRenaming: pane.renaming, - startTab: pane.tab, -@@ -12217,9 +12217,79 @@ var BROWSER_CSS = ` - @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} + { className: "bt-us" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-empty" }, +- (0, import_react24.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), +- (0, import_react24.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), +- (0, import_react24.createElement)("span", null, t("Token usage shows up here after a model is called.")) ++ (0, import_react38.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), ++ (0, import_react38.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), ++ (0, import_react38.createElement)("span", null, t("Token usage shows up here after a model is called.")) + ) + ); + } +@@ -9912,7 +9912,7 @@ function UsagePage({ roster, actions, focusId }) { + ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] + ]; + const whole = part.total.total; +- const ownerRow = ([at, sum], index, total = whole) => (0, import_react24.createElement)(Row2, { ++ const ownerRow = ([at, sum], index, total = whole) => (0, import_react38.createElement)(Row3, { + key: `o${at}`, + kind: faces[at].kind, + glyph: faces[at].glyph(32), +@@ -9924,7 +9924,7 @@ function UsagePage({ roster, actions, focusId }) { + fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), + onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) + }); +- const modelRow = ([at, sum], index) => (0, import_react24.createElement)(Row2, { ++ const modelRow = ([at, sum], index) => (0, import_react38.createElement)(Row3, { + key: `m${at}`, + kind: "model", + glyph: models[at].glyph(32), +@@ -9936,30 +9936,30 @@ function UsagePage({ roster, actions, focusId }) { + fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), + onClick: () => pick2({ kind: "model", key: view.models[at].key }) + }); +- const nothing = (0, import_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); ++ const nothing = (0, import_react38.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); + let body; + if (face) { + const kinds = ranked(part.kinds); + const usedModels = ranked(part.models); + const owners = ranked(part.owners); + body = [ +- (0, import_react24.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), ++ (0, import_react38.createElement)( + "div", + { key: "sheet", className: "bt-us-sheet" }, +- (0, import_react24.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), +- (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), ++ (0, import_react38.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), ++ (0, import_react38.createElement)( + "div", + { key: "pair", className: "bt-us-pair" }, +- ownerAt >= 0 ? (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react24.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), +- (0, import_react24.createElement)( ++ ownerAt >= 0 ? (0, import_react38.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react38.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), ++ (0, import_react38.createElement)( + ListCard, + { title: t("What for"), index: 3 }, +- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react24.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing ++ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react38.createElement)(Row3, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing + ) + ), +- (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) ++ (0, import_react38.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) + ) + ]; + } else { +@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) { + const rest = owners.filter(([at]) => !isBot(at)); + const usedModels = ranked(all.models); + body = [ +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { key: "sheet", className: "bt-us-sheet" }, +- (0, import_react24.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), +- (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), ++ (0, import_react38.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), ++ (0, import_react38.createElement)( + "div", + { key: "pair", className: "bt-us-pair" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + ListCard, + { title: t("Bots"), count: bots.length, index: 2 }, + owners.length > 0 ? null : nothing, + bots.map((entry, index) => ownerRow(entry, index)), +- bots.length > 0 && rest.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, ++ bots.length > 0 && rest.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, + rest.map((entry, index) => ownerRow(entry, bots.length + index)) + ), +- (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) ++ (0, import_react38.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) + ), +- (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) ++ (0, import_react38.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) + ) + ]; + } +- return (0, import_react24.createElement)( ++ return (0, import_react38.createElement)( + "div", + { ref: root, className: "bt-us", key: focusId ?? "all" }, +- (0, import_react24.createElement)( ++ (0, import_react38.createElement)( + "div", + { className: "bt-us-top" }, +- (0, import_react24.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), +- view.scanning ? (0, import_react24.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, +- picking ? (0, import_react24.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null ++ (0, import_react38.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), ++ view.scanning ? (0, import_react38.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, ++ picking ? (0, import_react38.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null + ), +- error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, ++ error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, + body + ); + } +@@ -10150,33 +10150,216 @@ button.bt-us-row:hover{background:var(--bt-hover)} + @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} + @keyframes bt-us-fade{from{opacity:0}} + @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} +-@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING)}}} ++@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING2)}}} + @keyframes bt-us-pulse{50%{opacity:.25}} + @keyframes bt-us-shine{from{background-position:200% 0}to{background-position:-200% 0}} + @media (prefers-reduced-motion:reduce){.bt-us *,.bt-us *::before{animation:none!important;transition:none!important}} `; -+// src/client/remote-bot-settings.js -+var import_react45 = require("react"); -+var REMOTE_BOT_SETTINGS_CSS = ` -+.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} -+.bt-remote-sheet-layer[data-leaving]{pointer-events:none} -+.bt-remote-sheet-layer .bt-scrim{position:absolute;inset:0;z-index:0;pointer-events:auto} -+.bt-remote-sheet-layer[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} -+.bt-remote-sheet:focus{outline:none} -+.bt-remote-sheet{position:relative;z-index:1;pointer-events:auto;width:min(320px,calc(100vw - 32px));max-height:min(70vh,420px);margin:0 auto 0;border-radius:14px;background:var(--bt-card,#fff);border:.5px solid var(--bt-line-2);box-shadow:0 12px 40px rgba(20,20,20,.16);display:flex;flex-direction:column;overflow:hidden;animation:bt-remote-sheet-in .2s ease-out} -+.bt-remote-sheet-layer[data-leaving] .bt-remote-sheet{animation:bt-remote-sheet-out .16s ease-in forwards} -+.bt-remote-sheet-head{display:flex;align-items:center;gap:10px;padding:12px 12px 8px} -+.bt-remote-sheet-who{display:flex;flex-direction:column;min-width:0;gap:1px;flex:1 1 auto} -+.bt-remote-sheet-name{font-size:16px;line-height:22px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-remote-sheet-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-remote-sheet-body{padding:0 12px 14px;overflow:auto} -+@keyframes bt-remote-sheet-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} -+@keyframes bt-remote-sheet-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(8px)}} -+@media (min-width:520px){ -+.bt-remote-sheet-layer{align-items:center;padding:24px} -+.bt-remote-sheet{margin:0} + // src/client/connectors.js +-var import_react25 = require("react"); ++var import_react39 = require("react"); + var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; +-var GitHubMark = () => (0, import_react25.createElement)( ++var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; ++var FEISHU_WEBHOOK_URL = "https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot"; ++var APPS = [ ++ ["gmail", "Gmail"], ++ ["googlecalendar", "Google Calendar"], ++ ["googledrive", "Google Drive"], ++ ["googledocs", "Google Docs"], ++ ["googlesheets", "Google Sheets"], ++ ["outlook", "Outlook"], ++ ["slack", "Slack"], ++ ["microsoft_teams", "Microsoft Teams"], ++ ["discord", "Discord"], ++ ["telegram", "Telegram"], ++ ["whatsapp", "WhatsApp"], ++ ["notion", "Notion"], ++ ["linear", "Linear"], ++ ["jira", "Jira"], ++ ["asana", "Asana"], ++ ["trello", "Trello"], ++ ["todoist", "Todoist"], ++ ["airtable", "Airtable"], ++ ["dropbox", "Dropbox"], ++ ["hubspot", "HubSpot"], ++ ["salesforce", "Salesforce"], ++ ["stripe", "Stripe"], ++ ["zoom", "Zoom"], ++ ["calendly", "Calendly"], ++ ["figma", "Figma"], ++ ["twitter", "X (Twitter)"], ++ ["youtube", "YouTube"] ++]; ++var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; ++var GitHubMark = () => (0, import_react39.createElement)( + "svg", + { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, +- (0, import_react25.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) ++ (0, import_react39.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) ++); ++var FeishuMark = () => (0, import_react39.createElement)( ++ "svg", ++ { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, ++ (0, import_react39.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }), ++ (0, import_react39.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }), ++ (0, import_react39.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" }) + ); +-var MARKS = { github: GitHubMark }; +-var ABOUT = { github: () => t("Repositories, issues, and pull requests") }; ++var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark }; ++var ABOUT = { ++ github: () => t("Repositories, issues, and pull requests"), ++ composio: () => t("Gmail, Slack, Calendar, Notion and many more apps, through one Composio key"), ++ feishu: () => t("Posts into one Feishu group through a custom bot webhook") ++}; + function statusText2(connector) { + if (connector.state === "connecting") return t("Connecting…"); + if (connector.state === "error") return t("Could not connect: {error}", { error: connector.error ?? "" }); ++ if (connector.state === "ready" && connector.kind === "channel") { ++ return connector.mode === "app" ? t("Connected: app, over the long connection") : t("Connected: custom bot webhook"); ++ } ++ if (connector.state === "ready" && connector.apps?.length > 0) return t("Connected: {apps}", { apps: connector.apps.map(appName).join(", ") }); + if (connector.state === "ready") return connector.account ? t("Connected as {account}", { account: connector.account }) : t("Connected"); + return ABOUT[connector.id]?.() ?? ""; + } +-function ConnectorRow({ connector, actions }) { +- const [form, setForm] = (0, import_react25.useState)(false); +- const [token, setToken] = (0, import_react25.useState)(""); +- const [busy, setBusy] = (0, import_react25.useState)(false); +- const [error, setError] = (0, import_react25.useState)(null); ++function CredentialField({ credential, value, onChange, onEnter, autoFocus }) { ++ const label = t(credential.label); ++ if (credential.masked) return (0, import_react39.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus }); ++ return (0, import_react39.createElement)( ++ "label", ++ { className: "bt-q-field" }, ++ (0, import_react39.createElement)("input", { ++ className: "bt-input", ++ type: "text", ++ value, ++ "aria-label": label, ++ placeholder: label, ++ spellCheck: false, ++ autoFocus, ++ onChange: (event) => onChange(event.target.value), ++ onKeyDown: (event) => { ++ if (event.key === "Enter") onEnter?.(); ++ } ++ }) ++ ); +} -+`; -+function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { -+ const close = () => actions.closeSettings(); -+ useEscape(close); -+ const dialog = (0, import_react45.useRef)(null); -+ (0, import_react45.useEffect)(() => { -+ const before = document.activeElement; -+ dialog.current?.focus?.({ preventScroll: true }); ++function AllowedSenders({ connector, actions, run, busy }) { ++ const [openId, setOpenId] = (0, import_react39.useState)(""); ++ const allowed = connector.allowed ?? []; ++ const refused = connector.inbound?.refusedSenders ?? []; ++ const allow = (id) => void run(actions.connectorAllow(connector.id, id)); ++ return (0, import_react39.createElement)( ++ "div", ++ { className: "bt-connector-allowed" }, ++ (0, import_react39.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")), ++ allowed.length === 0 && refused.length === 0 ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null, ++ allowed.map((entry) => (0, import_react39.createElement)( ++ "div", ++ { key: entry.openId, className: "bt-connector-person" }, ++ (0, import_react39.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block")) ++ )), ++ refused.map((entry) => (0, import_react39.createElement)( ++ "div", ++ { key: entry.openId, className: "bt-connector-person" }, ++ (0, import_react39.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow")) ++ )), ++ (0, import_react39.createElement)( ++ "div", ++ { className: "bt-connector-form-row" }, ++ (0, import_react39.createElement)("input", { ++ className: "bt-input", ++ type: "text", ++ value: openId, ++ spellCheck: false, ++ "aria-label": t("Feishu open_id"), ++ placeholder: t("Paste an open_id to allow"), ++ onChange: (event) => setOpenId(event.target.value), ++ onKeyDown: (event) => { ++ if (event.key === "Enter" && openId.trim() !== "") { ++ allow(openId.trim()); ++ setOpenId(""); ++ } ++ } ++ }), ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => { ++ allow(openId.trim()); ++ setOpenId(""); ++ } }, t("Allow")) ++ ) ++ ); ++} ++var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; ++function AppChip({ name, status, onRemove }) { ++ const label = APP_STATUS[status]?.(); ++ return (0, import_react39.createElement)( ++ "button", ++ { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, ++ name, ++ label ? (0, import_react39.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null ++ ); ++} ++function AppPicker({ apps, onChange, search, statuses = {} }) { ++ const [query, setQuery] = (0, import_react39.useState)(""); ++ const [found, setFound] = (0, import_react39.useState)(null); ++ const [failed, setFailed] = (0, import_react39.useState)(null); ++ const names = (0, import_react39.useRef)(new Map(APPS)); ++ const asked = (0, import_react39.useRef)(0); ++ (0, import_react39.useEffect)(() => { ++ const text2 = query.trim(); ++ if (text2 === "" || search === void 0) { ++ setFound(null); ++ setFailed(null); ++ return void 0; ++ } ++ const ask = ++asked.current; ++ const timer = setTimeout(() => { ++ search(text2).then((list) => { ++ if (ask !== asked.current) return; ++ for (const app of list) names.current.set(app.slug, app.name); ++ setFound(list); ++ setFailed(null); ++ }, (failure) => { ++ if (ask !== asked.current) return; ++ setFound(null); ++ setFailed(failure?.message ?? String(failure)); ++ }); ++ }, 250); ++ return () => clearTimeout(timer); ++ }, [query, search]); ++ const nameOf = (slug) => names.current.get(slug) ?? appName(slug); ++ const picked = new Set(apps); ++ const needle = query.trim().toLowerCase(); ++ const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); ++ const results = (found ?? local).filter((app) => !picked.has(app.slug)); ++ return (0, import_react39.createElement)( ++ "div", ++ { className: "bt-connector-apps" }, ++ apps.length > 0 ? (0, import_react39.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react39.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, ++ (0, import_react39.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), ++ failed ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, ++ (0, import_react39.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react39.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { ++ names.current.set(app.slug, app.name); ++ onChange([...apps, app.slug]); ++ } }, app.name))) ++ ); ++} ++function AppStatusList({ apps, statuses }) { ++ if (apps.length === 0 || Object.keys(statuses).length === 0) return null; ++ return (0, import_react39.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react39.createElement)( ++ "li", ++ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, ++ (0, import_react39.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), ++ (0, import_react39.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") ++ ))); ++} ++function ConnectorRow({ connector, roster, actions }) { ++ const [form, setForm] = (0, import_react39.useState)(false); ++ const [bots, setBots] = (0, import_react39.useState)(false); ++ const [token, setToken] = (0, import_react39.useState)(""); ++ const [apps, setApps] = (0, import_react39.useState)(() => connector.apps ?? []); ++ const [picking, setPicking] = (0, import_react39.useState)(false); ++ const [busy, setBusy] = (0, import_react39.useState)(false); ++ const [error, setError] = (0, import_react39.useState)(null); ++ const [message, setMessage] = (0, import_react39.useState)(""); + const run = (task, after) => { + setBusy(true); + setError(null); +@@ -10184,83 +10184,195 @@ function ConnectorRow({ connector, actions }) { + after?.(); + }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); + }; ++ const gateway = connector.id === "composio"; ++ const sender = connector.kind === "sender"; ++ const channel = connector.kind === "channel"; ++ const modes = connector.modes ?? []; ++ const [mode, setMode] = (0, import_react39.useState)(() => connector.mode ?? modes[0]?.id); ++ const [values, setValues] = (0, import_react39.useState)({}); ++ const chosen = modes.find((entry) => entry.id === mode) ?? modes[0]; ++ const field = (name) => values[name] ?? ""; ++ const setField = (name, value) => setValues((current) => ({ ...current, [name]: value })); ++ const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== ""); ++ const [statuses, setStatuses] = (0, import_react39.useState)({}); ++ const appsKey = (connector.apps ?? []).join(","); ++ (0, import_react39.useEffect)(() => { ++ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { ++ setStatuses({}); ++ return void 0; ++ } ++ let live = true; ++ actions.connectorAppStatus(connector.id).then((value) => { ++ if (live) setStatuses(value ?? {}); ++ }, () => { ++ if (live) setStatuses({}); ++ }); + return () => { -+ if (before && typeof before.focus === "function" && before.isConnected) before.focus({ preventScroll: true }); ++ live = false; + }; -+ }, []); -+ const onKeyDown = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ close(); ++ }, [gateway, connector.state, appsKey, picking]); ++ const search = (0, import_react39.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); + const connect = () => { +- if (busy || token.trim() === "") return; +- void run(actions.connectorConnect(connector.id, token), () => { ++ if (busy) return; ++ if (channel) { ++ if (chosen === void 0 || !filled) return; ++ const credentials = Object.fromEntries(chosen.credentials.map((credential) => [credential.name, field(credential.name)])); ++ void run(actions.connectorConnect(connector.id, { mode: chosen.id, credentials }), () => { ++ setValues({}); ++ setForm(false); ++ }); ++ return; + } ++ if (token.trim() === "") return; ++ if (gateway && apps.length === 0) { ++ setError(t("Pick at least one app")); ++ return; ++ } ++ void run(actions.connectorConnect(connector.id, token, gateway ? apps : void 0), () => { + setToken(""); + setForm(false); + }); + }; ++ const saveApps = () => { ++ if (busy) return; ++ if (apps.length === 0) { ++ setError(t("Pick at least one app")); ++ return; ++ } ++ void run(actions.connectorApps(connector.id, apps), () => setPicking(false)); + }; -+ if (!bot?.remote) return null; -+ const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); -+ return (0, import_react45.createElement)( -+ "div", -+ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, -+ (0, import_react45.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react45.createElement)( -+ "div", -+ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, -+ (0, import_react45.createElement)( + const on = connector.state !== "off"; ++ const canPost = sender || channel && connector.mode === "webhook"; ++ const perBot = !sender && (!channel || connector.mode === "app"); + const Mark = MARKS[connector.id]; +- const row = (0, import_react25.useRef)(null); +- useMorph(row, `${form}:${error !== null}:${connector.state}`); +- return (0, import_react25.createElement)( ++ const row = (0, import_react39.useRef)(null); ++ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`); ++ return (0, import_react39.createElement)( + "div", + { ref: row, className: "bt-connector", "data-state": connector.state }, +- (0, import_react25.createElement)( ++ (0, import_react39.createElement)( + "div", + { className: "bt-connector-head" }, +- (0, import_react25.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react25.createElement)(Mark) : null), +- (0, import_react25.createElement)( ++ (0, import_react39.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react39.createElement)(Mark) : null), ++ (0, import_react39.createElement)( + "span", + { className: "bt-connector-text" }, +- (0, import_react25.createElement)("span", { className: "bt-connector-name" }, connector.name), +- (0, import_react25.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) ++ (0, import_react39.createElement)("span", { className: "bt-connector-name" }, connector.name), ++ (0, import_react39.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) + ), +- (0, import_react25.createElement)( ++ (0, import_react39.createElement)( + "span", + { className: "bt-connector-actions" }, +- connector.state === "error" ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, +- on ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) ++ connector.state === "error" ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, ++ on && gateway && !picking ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ setApps(connector.apps ?? []); ++ setPicking(true); ++ } }, t("Choose apps")) : null, ++ on && perBot ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, ++ on ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) + ) + ), +- !on && form ? (0, import_react25.createElement)( ++ on && gateway && !picking ? (0, import_react39.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, ++ on && perBot ? (0, import_react39.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, ++ on && bots ? (0, import_react39.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, ++ on && channel && connector.mode === "app" ? (0, import_react39.createElement)(AllowedSenders, { connector, actions, run, busy }) : null, ++ on && canPost ? (0, import_react39.createElement)( + "div", + { className: "bt-connector-form" }, +- (0, import_react25.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react25.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: t("GitHub token"), autoFocus: true })), +- (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-remote-sheet-head" }, -+ (0, import_react45.createElement)(BotMark, { bot, size: 40, live: true }), -+ (0, import_react45.createElement)( -+ "div", -+ { className: "bt-remote-sheet-who" }, -+ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), -+ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-sub" }, who) ++ { 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")) ++ ) ++ ) : null, ++ !on && form ? (0, import_react39.createElement)( ++ "div", ++ { className: "bt-connector-form" }, ++ channel ? (0, import_react39.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react39.createElement)("button", { ++ key: entry.id, ++ type: "button", ++ className: "bt-state", ++ "aria-pressed": entry.id === chosen?.id, ++ onClick: () => { ++ setMode(entry.id); ++ setValues({}); ++ setError(null); ++ } ++ }, t(entry.label)))) : null, ++ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react39.createElement)(CredentialField, { ++ key: credential.name, ++ credential, ++ value: field(credential.name), ++ onChange: (value) => setField(credential.name, value), ++ onEnter: connect, ++ autoFocus: index === 0 ++ })) : (0, import_react39.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react39.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), ++ gateway ? (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, ++ (0, import_react39.createElement)( + "div", + { className: "bt-connector-form-row" }, +- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ (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" }, ++ channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub") + ), -+ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react45.createElement)(CloseIcon)) -+ ), -+ (0, import_react45.createElement)( -+ "div", -+ { className: "bt-remote-sheet-body" }, -+ bot.remote.handle ? (0, import_react45.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, -+ (0, import_react45.createElement)(FlyBotDetails, { bot, roster, actions }) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + setForm(false); + setToken(""); ++ setValues({}); ++ setError(null); ++ } }, t("Cancel")), ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) + ) -+ ) -+ ); -+} -+ - // src/client/overlays.js --var import_react33 = require("react"); --var import_react_dom9 = require("react-dom"); -+var import_react46 = require("react"); -+var import_react_dom11 = require("react-dom"); - var DIALOG_EXIT_MS = 160; - var MENU_EXIT_MS3 = 120; - function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -@@ -12227,15 +12227,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity - const ui2 = useUi((value) => value); - useRegistry((value) => value); - const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); -- const previous = (0, import_react33.useRef)(current); -- (0, import_react33.useEffect)(() => { -+ const previous = (0, import_react46.useRef)(current); -+ (0, import_react46.useEffect)(() => { - if (previous.current === current) return; - previous.current = current; - if (current) actions.followPanel?.(current); - }, [current]); - const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; -- const [kept, setKept] = (0, import_react33.useState)(open); -- (0, import_react33.useEffect)(() => { -+ const [kept, setKept] = (0, import_react46.useState)(open); -+ (0, import_react46.useEffect)(() => { - if (open) { - setKept(open); - return void 0; -@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity - }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); - const pane = open ?? kept; - const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); -+ const remoteBotId = ui2.remoteBotSettings && roster.byId[ui2.remoteBotSettings]?.remote ? ui2.remoteBotSettings : null; -+ const [remoteBotSettings, remoteLeaving] = useLinger(remoteBotId, DIALOG_EXIT_MS); - const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS); - const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); -- return (0, import_react33.createElement)( -- import_react33.Fragment, -+ const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); -+ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); -+ return (0, import_react46.createElement)( -+ import_react46.Fragment, - null, -- pane ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, -- ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, -- ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, -- ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, -- ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, -- menu ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, -- settings ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, -- memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null -+ pane ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, -+ ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, -+ ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, -+ ui2.exchange ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, -+ ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, -+ menu ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, -+ settings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, -+ remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, -+ memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, -+ inbox ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, -+ board ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null ++ ) : null, ++ on && gateway && picking ? (0, import_react39.createElement)( ++ "div", ++ { className: "bt-connector-form" }, ++ (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), ++ (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), ++ (0, import_react39.createElement)( ++ "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, onClick: () => { ++ setPicking(false); + setError(null); + } }, t("Cancel")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) + ) + ) : null, +- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ++ error ? (0, import_react39.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null + ); + } + function ConnectorsPage({ roster, actions }) { + const list = roster?.connectors ?? []; +- return (0, import_react25.createElement)( ++ return (0, import_react39.createElement)( + "section", + { className: "bt-set-section" }, +- (0, import_react25.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react25.createElement)(ConnectorRow, { key: connector.id, connector, actions }))) ++ (0, import_react39.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react39.createElement)("div", { className: "bt-set-row" }, (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react39.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) + ); + } + + // src/client/feedback.js +-var import_react26 = require("react"); +-var FeedbackIcon = () => (0, import_react26.createElement)( ++var import_react40 = require("react"); ++var FeedbackIcon = () => (0, import_react40.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_react26.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), +- (0, import_react26.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) ++ (0, import_react40.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), ++ (0, import_react40.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) + ); +-function download(name, text) { +- const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); ++function download(name, text2) { ++ const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = name; + anchor.click(); + setTimeout(() => URL.revokeObjectURL(url), 1e3); + } +-var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); +-function whenText(time) { ++var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); ++function whenText2(time) { + 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) { + if (entry.source === "turn") return failureLabel(entry.code, entry.text); + return entry.text; + } +-var SOURCES = { turn: "Model call", host: "DS Bot", api: "DS Bot", browser: "This window" }; +-function FeedbackPage({ actions, run, Section, Row: Row3 }) { +- const [what, setWhat] = (0, import_react26.useState)(""); +- const [report, setReport] = (0, import_react26.useState)(null); +- const [preview, setPreview] = (0, import_react26.useState)(false); +- const [note, setNote] = (0, import_react26.useState)(""); ++var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" }; ++function FeedbackPage({ actions, run, Section, Row: Row4 }) { ++ const [what, setWhat] = (0, import_react40.useState)(""); ++ const [report, setReport] = (0, import_react40.useState)(null); ++ 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) => { + setReport(next); + return next; + }); + const load = () => fetchReport().then(() => void 0); +- (0, import_react26.useEffect)(() => { ++ (0, import_react40.useEffect)(() => { + void run(load()); + }, []); + const copyReport = () => run(fetchReport().then(async (next) => { +@@ -10289,7 +10289,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { + setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); + if (!copied) setPreview(true); + })); +- const [issueLink, setIssueLink] = (0, import_react26.useState)(null); ++ const [issueLink, setIssueLink] = (0, import_react40.useState)(null); + const openIssue = () => { + const win = window.open("", "_blank"); + if (win !== null) win.opener = null; +@@ -10307,8 +10307,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { + if (!inline && !copied) setPreview(true); + })); + }; +- const downloadLog = () => run(actions.diagnosticsLog().then((text) => { +- download("ds-bot-diagnostics.log", text || "No entries.\n"); ++ const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { ++ download("ds-bot-diagnostics.log", text2 || "No entries.\n"); + 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 total = (report?.total ?? 0) + browserErrors.length; + const shown = entries2.slice(0, 8); + return [ +- (0, import_react26.createElement)( ++ (0, import_react40.createElement)( + Section, + { key: "report", title: t("Report a problem") }, +- (0, import_react26.createElement)( ++ (0, import_react40.createElement)( + "div", + { className: "bt-set-row bt-set-paste bt-fb-what" }, +- (0, import_react26.createElement)( ++ (0, import_react40.createElement)( + "label", + { className: "bt-set-copy", htmlFor: "bt-fb-what" }, +- (0, import_react26.createElement)("span", null, t("What happened?")), +- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) ++ (0, import_react40.createElement)("span", null, t("What happened?")), ++ (0, import_react40.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) + ), +- (0, import_react26.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) ++ (0, import_react40.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) + ), +- (0, import_react26.createElement)( +- Row3, ++ (0, import_react40.createElement)( ++ Row4, + { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, +- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), +- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) + ), +- preview ? (0, import_react26.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react26.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, +- (0, import_react26.createElement)( +- Row3, +- { label: t("GitHub issue"), hint: t("Opens a new issue in the DS Bot repository with all of this filled in. Nothing is sent until you submit it there.") }, +- (0, import_react26.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) ++ preview ? (0, import_react40.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react40.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, ++ (0, import_react40.createElement)( ++ Row4, ++ { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) + ) + ), +- note ? (0, import_react26.createElement)( ++ note ? (0, import_react40.createElement)( + "div", + { key: "note", className: "bt-note bt-fb-note", role: "status" }, + note, +- issueLink ? [" ", (0, import_react26.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null ++ issueLink ? [" ", (0, import_react40.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null + ) : null, +- (0, import_react26.createElement)( ++ (0, import_react40.createElement)( + Section, + { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, +- shown.length === 0 ? (0, import_react26.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and DS Bot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react26.createElement)( ++ shown.length === 0 ? (0, import_react40.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react40.createElement)( + "div", + { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, +- (0, import_react26.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), +- (0, import_react26.createElement)( ++ (0, import_react40.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), ++ (0, import_react40.createElement)( + "div", + { className: "bt-set-copy" }, +- (0, import_react26.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), +- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "DS Bot")].filter(Boolean).join(" · ")) ++ (0, import_react40.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), ++ (0, import_react40.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) + ), +- entry.code ? (0, import_react26.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null ++ entry.code ? (0, import_react40.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null + )), +- (0, import_react26.createElement)( +- Row3, ++ (0, import_react40.createElement)( ++ Row4, + { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, +- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), +- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) + ) + ) + ]; +@@ -10377,8 +10377,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { + var TOAST_MS = 6e3; + function ErrorToast({ useUi, actions }) { + const toast = useUi((state) => state.toast); +- const [leaving, setLeaving] = (0, import_react26.useState)(false); +- (0, import_react26.useEffect)(() => { ++ const [leaving, setLeaving] = (0, import_react40.useState)(false); ++ (0, import_react40.useEffect)(() => { + if (!toast) return void 0; + setLeaving(false); + const fade = setTimeout(() => setLeaving(true), TOAST_MS); +@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) { + }; + }, [toast?.token]); + if (!toast) return null; +- return (0, import_react26.createElement)( ++ const info = toast.tone === "info"; ++ return (0, import_react40.createElement)( + "div", +- { className: "bt-toast", role: "alert", "data-leaving": leaving || void 0 }, +- (0, import_react26.createElement)("span", { className: "bt-toast-text" }, toast.text), +- (0, import_react26.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { ++ { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, ++ (0, import_react40.createElement)("span", { className: "bt-toast-text" }, toast.text), ++ info ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { + actions.dismissToast(toast.token); + actions.openSettings("feedback"); + } }, t("Details")), +- (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react26.createElement)(CloseIcon)) ++ (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react40.createElement)(CloseIcon)) ); } - -@@ -12294,19 +12294,19 @@ function readPage(limits) { - const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); - return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); - }; -- const keep = (text2, end = "") => { -- if (text2 === "") return ""; -- stash.push(text2); -+ const keep = (text3, end = "") => { -+ if (text3 === "") return ""; -+ stash.push(text3); - return ` - - ${stash.length - 1}${end} - + var FEEDBACK_CSS = ` +@@ -10416,47 +10416,56 @@ var FEEDBACK_CSS = ` + .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} + .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} + .bt-event-error{color:#e02135} +-.bt-toast{position:fixed;left:50%;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100vw - 32px));padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;transform:translateX(-50%);animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-toast[data-leaving]{opacity:0;transform:translate(-50%,6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} ++.bt-toast{position:fixed;left:0;right:0;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;width:fit-content;max-width:min(560px,calc(100vw - 32px));margin-inline:auto;padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-toast[data-leaving]{opacity:0;transform:translateY(6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} + .bt-toast-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical} + .bt-toast-text::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#e02135;margin:0 8px 1px 0} + .bt-toast-link{flex:none;border:0;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;padding:4px 6px;border-radius:6px} + .bt-toast-link:hover{background:var(--bt-hover);color:var(--bt-ink)} + .bt-toast-close{flex:none} +-@keyframes bt-toast-in{from{transform:translate(-50%,16px) scale(.94)}} ++@keyframes bt-toast-in{from{transform:translateY(16px) scale(.94)}} + @media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}} `; + + // src/client/about.js +-var import_react27 = require("react"); +-var CHEER_MS = 1800; ++var import_react41 = require("react"); + var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); + function botHint(update) { +- if (update.phase === "installing") return t("Updating DS Bot…"); +- if (update.phase === "installed") return t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }); ++ if (update.phase === "installing") return t("Updating FlyBot core…"); ++ if (update.phase === "installed") return t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }); + if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") }); +- if (update.available) return t("DS Bot {version} is available", { version: update.latest }); ++ if (update.available) return t("FlyBot core {version} is available", { version: update.latest }); + if (update.enabled === false) return t("Update checks are off."); + if (update.source === "path") return t("Installed from a local folder; update it there."); ++ if (update.source === "git" && update.build?.branch !== void 0) return branchHint(update); ++ if (update.source === "git" && update.build?.pinned) return t("Pinned to commit {commit}. Updates come from the maintainer: reinstall FlyBot to get them.", { commit: update.build.commit }); + if (!update.checkedAt) return null; + if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install."); +- const checked = t("Checked {time}", { time: whenText(update.checkedAt) }); ++ const checked = t("Checked {time}", { time: whenText2(update.checkedAt) }); + return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`; + } ++function branchHint({ build, behind, checkedAt }) { ++ const where = [build.branch, build.commit].filter(Boolean).join(" "); ++ if (build.commit === void 0 || !checkedAt) return t("Installed from GitHub ({where}). Updates come from the maintainer: reinstall FlyBot to get them.", { where }); ++ const checked = t("Checked {time}", { time: whenText2(checkedAt) }); ++ const state = behind === null ? t("Could not compare with {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : behind === 0 ? t("Up to date with {branch}.", { branch: build.branch }) : behind === 1 ? t("1 commit behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : t("{count} commits behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { count: behind, branch: build.branch }); ++ return `${state} · ${checked}`; ++} ++var versionLine = (update) => [t("Version {version}", { version: update.current }), [update.build?.branch, update.build?.commit].filter(Boolean).join(" ")].filter(Boolean).join(" · "); + function dshHint(dsh, shown) { + if (dsh.phase === "installing") return t("Updating DSH…"); + if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }); + if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") }); +- if (dsh.available === false && dsh.blockedBy) return t("Update DS Bot {version} first.", { version: dsh.blockedBy }); ++ if (dsh.available === false && dsh.blockedBy) return t("Update FlyBot core {version} first.", { version: dsh.blockedBy }); + if (shown) return t("DSH {version} is available", { version: dsh.verified }); + return t("Version {version}", { version: dsh.current }); + } + var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); +-function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { ++function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) { + const update = useUpdate((value) => value) ?? {}; +- const [checking, setChecking] = (0, import_react27.useState)(false); +- const [stars, setStars] = (0, import_react27.useState)(null); +- (0, import_react27.useEffect)(() => { ++ const [checking, setChecking] = (0, import_react41.useState)(false); ++ const [stars, setStars] = (0, import_react41.useState)(null); ++ (0, import_react41.useEffect)(() => { + let live = true; + actions.repoStars().then((count) => { + if (live && Number.isInteger(count)) setStars(count); +@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { + live = false; + }; + }, []); +- const [cheer, setCheer] = (0, import_react27.useState)(true); +- (0, import_react27.useEffect)(() => { +- if (!cheer) return void 0; +- const timer = setTimeout(() => setCheer(false), CHEER_MS); +- return () => clearTimeout(timer); +- }, [cheer]); + const check = () => { + setChecking(true); +- void run(actions.checkUpdate()).then((done) => { ++ void run(actions.checkUpdate()).then(() => { + setChecking(false); +- if (done) setCheer(true); + }); }; -- const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); -- const flow = (text2) => text2.split("\n").map((line) => line.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); -- const block = (text2) => expand(flow(text2)); -- const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); -+ const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); -+ const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); -+ const block = (text3) => expand(flow(text3)); -+ const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); - const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); - const childrenOf = (el) => { - if (el.shadowRoot) return el.shadowRoot.childNodes; -@@ -12340,8 +12340,8 @@ function readPage(limits) { - items.push(item); - return { item, number: items.length, secret }; - }; -- const nameOf = (el, text2, secret) => collapse( -- el.getAttribute("aria-label") || text2 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" -+ const nameOf = (el, text3, secret) => collapse( -+ el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" - ).slice(0, 80); - const fenced = (code, lang) => { - const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); -@@ -12372,7 +12372,7 @@ ${fence}`; - const marker = ordered ? `${number++}. ` : "- "; - if (body2 === "") continue; - const pad2 = " ".repeat(marker.length); -- lines.push(body2.split("\n").map((line, index) => (index === 0 ? marker : line ? pad2 : "") + line).join("\n")); -+ lines.push(body2.split("\n").map((line2, index) => (index === 0 ? marker : line2 ? pad2 : "") + line2).join("\n")); - } - return keep(lines.join("\n")); + const phase = update.phase ?? "idle"; + const dsh = update.dsh; + const dshPhase = dsh?.phase ?? "idle"; +- (0, import_react27.useEffect)(() => { +- if (phase === "installed" || dshPhase === "installed") setCheer(true); +- }, [phase, dshPhase]); +- const markState = checking ? "searching" : phase === "installing" || dshPhase === "installing" ? "working" : cheer ? "done" : "idle"; + const dshNew = showsDshRow(dsh, update.app); + const dshShown = dshNew && dsh.available; +- const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react27.createElement)( ++ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react41.createElement)( + "button", + { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, + phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") +- ) : (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); +- const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react27.createElement)( ++ ) : (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); ++ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react41.createElement)( + "button", + { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, + dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") + ) : null; + const repo = update.repoUrl; + return [ +- (0, import_react27.createElement)( ++ (0, import_react41.createElement)( + "div", + { key: "hero", className: "bt-about-hero" }, +- (0, import_react27.createElement)("span", { className: "bt-about-mark" }, (0, import_react27.createElement)(BotMark, { look: MAIN_LOOK, bot: { id: "about" }, size: 52, state: markState, live: true, gaze: true, pokeable: true })), +- (0, import_react27.createElement)( ++ (0, import_react41.createElement)("span", { className: "bt-about-mark" }, (0, import_react41.createElement)(FlyBotMark, { size: 52 })), ++ (0, import_react41.createElement)( + "div", + { className: "bt-about-copy" }, +- (0, import_react27.createElement)("span", { className: "bt-about-name" }, "DS Bot"), +- update.current ? (0, import_react27.createElement)("span", { className: "bt-about-version" }, t("Version {version}", { version: update.current })) : null ++ (0, import_react41.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), ++ update.current ? (0, import_react41.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null + ), +- repo ? (0, import_react27.createElement)( ++ repo ? (0, import_react41.createElement)( + "a", + { + className: "bt-star", + href: repo, + target: "_blank", + rel: "noopener noreferrer", +- title: t("Star DS Bot on GitHub"), +- "aria-label": stars === null ? t("Star DS Bot on GitHub") : `${t("Star DS Bot on GitHub")} · ${t("{count} stars", { count: stars })}` ++ title: t("Star FlyBot on GitHub"), ++ "aria-label": stars === null ? t("Star FlyBot on GitHub") : `${t("Star FlyBot on GitHub")} · ${t("{count} stars", { count: stars })}` + }, +- (0, import_react27.createElement)("span", { className: "bt-star-main" }, (0, import_react27.createElement)("span", { className: "bt-star-icon" }, (0, import_react27.createElement)(StarLineIcon)), "Star"), +- stars === null ? null : (0, import_react27.createElement)("span", { className: "bt-star-count" }, starCount(stars)) ++ (0, import_react41.createElement)("span", { className: "bt-star-main" }, (0, import_react41.createElement)("span", { className: "bt-star-icon" }, (0, import_react41.createElement)(StarLineIcon)), "Star"), ++ stars === null ? null : (0, import_react41.createElement)("span", { className: "bt-star-count" }, starCount(stars)) + ) : null + ), +- repo ? (0, import_react27.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If DS Bot helps you, a star on GitHub helps more people find it.")) : null, +- (0, import_react27.createElement)( ++ repo ? (0, import_react41.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, ++ (0, import_react41.createElement)( + Section, + { key: "updates", title: t("Updates") }, +- (0, import_react27.createElement)(Row3, { label: "DS Bot", hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), +- dsh?.current ? (0, import_react27.createElement)(Row3, { label: "DeepSeek Harness", hint: dshHint(dsh, dshShown) }, dshControl) : null ++ (0, import_react41.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react41.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), ++ dsh?.current ? (0, import_react41.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null + ), +- (0, import_react27.createElement)( ++ (0, import_react41.createElement)( + Section, + { key: "links", title: t("Links") }, +- repo ? (0, import_react27.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react27.createElement)("a", linkProps(repo), (0, import_react27.createElement)(GitHubMark), t("Open"))) : null, +- update.notesUrl ? (0, import_react27.createElement)(Row3, { label: t("Release notes") }, (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, +- (0, import_react27.createElement)(Row3, { label: t("Report a problem") }, (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) ++ repo ? (0, import_react41.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react41.createElement)("a", linkProps(repo), (0, import_react41.createElement)(GitHubMark), t("Open"))) : null, ++ update.notesUrl ? (0, import_react41.createElement)(Row4, { label: t("Release notes") }, (0, import_react41.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, ++ (0, import_react41.createElement)(Row4, { label: t("Report a problem") }, (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) + ) + ]; + } + var ABOUT_CSS = ` + .bt-about-hero{display:flex;align-items:center;gap:14px;padding:16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} +-.bt-about-mark{flex:none;display:inline-flex;position:relative} +-.bt-about-mark::before{content:'';position:absolute;inset:-20%;border-radius:50%} +-body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle]:not(.bt-mark-poke) .bt-b{rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} +-body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye{scale:1 0;animation:none} +-body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye-happy{opacity:1;transition-delay:.1s} ++.bt-about-mark{flex:none;display:inline-flex} + .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} + .bt-about-name{font-size:16px;line-height:22px;font-weight:600} + .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} +@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s + `; + + // src/client/settings.js +-var import_react28 = require("react"); ++var import_react42 = require("react"); + var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); +-var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About DS Bot"]]; +-var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; ++var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["projects", "Projects"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["workflows", "Workflows"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; ++var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; + function SetSection({ title, children }) { +- return (0, import_react28.createElement)("section", { className: "bt-set-section" }, (0, import_react28.createElement)("h3", null, title), (0, import_react28.createElement)("div", { className: "bt-set-card" }, children)); ++ return (0, import_react42.createElement)("section", { className: "bt-set-section" }, (0, import_react42.createElement)("h3", null, title), (0, import_react42.createElement)("div", { className: "bt-set-card" }, children)); + } + function SetRow({ label, hint, children }) { +- return (0, import_react28.createElement)( ++ return (0, import_react42.createElement)( + "div", + { className: "bt-set-row" }, +- (0, import_react28.createElement)("div", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", null, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), +- children ? (0, import_react28.createElement)("div", { className: "bt-set-control" }, children) : null ++ (0, import_react42.createElement)("div", { className: "bt-set-copy" }, (0, import_react42.createElement)("span", null, label), hint ? (0, import_react42.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ children ? (0, import_react42.createElement)("div", { className: "bt-set-control" }, children) : null + ); + } ++var NOTIFY_KINDS = [ ++ ["approval", "Waiting for approval", "Once more if it still waits after 15 min."], ++ ["finished", "Finished"], ++ ["failed", "Failed"] ++]; + function GeneralPage({ roster, actions, run }) { + const prefs = roster.prefs ?? {}; + const themes = allThemes(); + const current = themes[prefs.theme] ? prefs.theme : "deepseek"; +- const [custom, setCustom] = (0, import_react28.useState)(prefs.accent ?? "#4d6bfe"); +- const timer = (0, import_react28.useRef)(0); +- (0, import_react28.useEffect)(() => () => clearTimeout(timer.current), []); ++ const [custom, setCustom] = (0, import_react42.useState)(prefs.accent ?? "#4d6bfe"); ++ const timer = (0, import_react42.useRef)(0); ++ (0, import_react42.useEffect)(() => () => clearTimeout(timer.current), []); + const pickCustom = (value) => { + setCustom(value); + clearTimeout(timer.current); + timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350); }; -@@ -12398,11 +12398,11 @@ ${children(cell, ctx)} - let width = 0; - for (const row of rows.slice(0, limits.tableRows + 1)) { - if (over()) break; -- const line = []; -+ const line2 = []; - let column = 0; - const place = () => { - while (carry[column]?.left > 0) { -- line[column] = carry[column].text; -+ line2[column] = carry[column].text; - carry[column].left -= 1; - column += 1; - } -@@ -12410,18 +12410,18 @@ ${children(cell, ctx)} - for (const cell of row.cells) { - place(); - if (styleOf(cell).display === "none") continue; -- const text2 = cellText(cell, ctx); -+ const text3 = cellText(cell, ctx); - const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); - const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); - for (let offset = 0; offset < span; offset += 1) { -- line[column] = offset === 0 ? text2 : ""; -- if (down > 1) carry[column] = { text: offset === 0 ? text2 : "", left: down - 1 }; -+ line2[column] = offset === 0 ? text3 : ""; -+ if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; - column += 1; + return [ +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetSection, + { key: "look", title: t("Appearance") }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetRow, + { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "select", + { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, +- Object.entries(themes).map(([id, theme]) => (0, import_react28.createElement)("option", { key: id, value: id }, t(theme.label))) ++ Object.entries(themes).map(([id, theme]) => (0, import_react42.createElement)("option", { key: id, value: id }, t(theme.label))) + ) + ), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetRow, + { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "div", + { className: "bt-swatches" }, +- (0, import_react28.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), +- ACCENT_INKS.map((ink) => (0, import_react28.createElement)("button", { ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), ++ ACCENT_INKS.map((ink) => (0, import_react42.createElement)("button", { + key: ink, + type: "button", + className: "bt-swatch", +@@ -10626,75 +10626,86 @@ function GeneralPage({ roster, actions, run }) { + style: { background: INKS[ink][0] }, + onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) + })), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "label", + { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, +- (0, import_react28.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) ++ (0, import_react42.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) + ) + ) + ) + ), +- (0, import_react28.createElement)( ++ // One switch for every system notification (#353), then one per kind (#469). ++ (0, import_react42.createElement)( + SetSection, +- { key: "mode", title: t("Mode") }, +- (0, import_react28.createElement)( +- SetRow, +- { label: t("Classic Agent mode") }, +- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch")) +- ) ++ { key: "notify", title: t("Notifications") }, ++ (0, import_react42.createElement)(SwitchRow, { ++ label: t("In other chats"), ++ hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), ++ on: prefs.notifications !== false, ++ onChange: (on) => run(actions.setPrefs({ notifications: on })) ++ }), ++ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react42.createElement)(SwitchRow, { ++ key: kind, ++ label: t(label), ++ hint: hint ? t(hint) : void 0, ++ disabled: prefs.notifications === false, ++ on: prefs.notify?.[kind] !== false, ++ onChange: (on) => run(actions.setPrefs({ notify: { [kind]: on } })) ++ })) + ) + ]; + } + function MainBotsSection({ roster, actions, run }) { + const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean); + const [first, ...others] = mains; +- const candidates = roster.bots.filter((bot) => !isMainOf(roster, bot.id)); +- return (0, import_react28.createElement)( ++ const own = roster.bots.filter((bot) => !bot.remote); ++ const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); ++ return (0, import_react42.createElement)( + SetSection, + { title: t("Main Bot") }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetRow, + { + label: t("Main Bot"), + hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 + }, +- (0, import_react28.createElement)("select", { ++ (0, import_react42.createElement)("select", { + className: "bt-select", + "aria-label": t("Main Bot"), + value: first?.id ?? "", + onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) +- }, first ? null : (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), roster.bots.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ++ }, first ? null : (0, import_react42.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react42.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) + ), +- others.map((bot) => (0, import_react28.createElement)( ++ others.map((bot) => (0, import_react42.createElement)( + SetRow, + { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, +- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), +- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) + )), +- candidates.length > 0 ? (0, import_react28.createElement)( ++ candidates.length > 0 ? (0, import_react42.createElement)( + SetRow, + { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, +- (0, import_react28.createElement)("select", { ++ (0, import_react42.createElement)("select", { + className: "bt-select", + "aria-label": t("Add a Main Bot"), + value: "", + onChange: (event) => { + if (event.target.value) void run(actions.setMain(event.target.value, true)); } - } - place(); -- width = Math.max(width, line.length); -- grid.push(line); -+ width = Math.max(width, line2.length); -+ grid.push(line2); - } - width = Math.min(width, limits.tableCols); - if (grid.length === 0 || width === 0) return caption ? ` -@@ -12480,9 +12480,9 @@ ${body2 || "(no text)"} - if (node.nodeType === 3) { - if (!ctx.shown) return ""; - const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); -- const text2 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); -- chars += text2.length; -- return text2; -+ const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); -+ chars += text3.length; -+ return text3; - } - if (node.nodeType !== 1) return ""; - const el = node; -@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} - } - if (tag === "blockquote") { - const quoted = block(body2); -- return quoted ? keep(quoted.split("\n").map((line) => line ? `> ${line}` : ">").join("\n")) : ""; -+ return quoted ? keep(quoted.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n")) : ""; - } - const end = body2.trimEnd().length; - const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -@@ -12570,12 +12570,12 @@ ${marked} - return marked; - } - const body = document.body; -- let text = ""; -+ let text2 = ""; - if (body) { -- text = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); -+ text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); - } -- if (text.length > limits.chars) { -- text = text.slice(0, limits.chars); -+ if (text2.length > limits.chars) { -+ text2 = text2.slice(0, limits.chars); - stop ||= `the text passed ${limits.chars} characters`; - } - const page = { -@@ -12585,7 +12585,7 @@ ${marked} - description: document.querySelector('meta[name="description"]')?.content || "", - selection: String(getSelection() || "").slice(0, 4e3), - format: "markdown", -- text, -+ text: text2, - items, - stats: { ...stats, ms: Math.round(performance.now() - started) } - }; -@@ -12613,15 +12613,15 @@ var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.in - var BLOCKED = "Cloud metadata addresses do not open here."; - var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); - function browserAddress(input) { -- const text = String(input ?? "").trim(); -- if (text === "") return null; -- if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) return /^https?:\/\//i.test(text) && URL.canParse(text) ? new URL(text).href : null; -- if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text)) return null; -- const host = text.split(/[/?#]/)[0]; -+ const text2 = String(input ?? "").trim(); -+ if (text2 === "") return null; -+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; -+ if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; -+ const host = text2.split(/[/?#]/)[0]; - const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); -- const looksLikeHost = !/\s/.test(text) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); -- if (looksLikeHost && URL.canParse(`http://${text}`)) return new URL(`${local ? "http" : "https"}://${text}`).href; -- return `${SEARCH_URL}${encodeURIComponent(text)}`; -+ const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); -+ if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; -+ return `${SEARCH_URL}${encodeURIComponent(text2)}`; +- }, (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ++ }, (0, import_react42.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react42.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) + ) : null + ); } - function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { - const views = /* @__PURE__ */ new Map(); -@@ -13000,63 +13000,1026 @@ function createBotBrowsers({ bridge, createSource: createSource2, storage = glob - } - }; + var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]]; + function MotionSection({ roster, actions, run }) { + const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; +- return (0, import_react28.createElement)( ++ return (0, import_react42.createElement)( + SetSection, + { title: t("Animation") }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetRow, + { label: t("Bot animation") }, +- (0, import_react28.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react28.createElement)("button", { ++ (0, import_react42.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react42.createElement)("button", { + key: id, + type: "button", + "aria-pressed": motion === id, +@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) { + ); } --var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); --function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.randomUUID(), retryMs = 1e3 }) { -- let stopped = false; -- let poll; -- let reported = ""; -- const answered = /* @__PURE__ */ new Map(); -- const answer = async ({ id, botId }) => { -- let job = answered.get(id); -- if (job === void 0) { -- job = (async () => { -- try { -- const result = { id, ok: true, page: await browsers.read(botId) }; -- browsers.noteRead(botId); -- return result; -- } catch (error) { -- return { id, ok: false, error: error instanceof Error ? error.message : String(error) }; -+var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -+function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.randomUUID(), retryMs = 1e3 }) { -+ let stopped = false; -+ let poll; -+ let reported = ""; -+ const answered = /* @__PURE__ */ new Map(); -+ const answer = async ({ id, botId }) => { -+ let job = answered.get(id); -+ if (job === void 0) { -+ job = (async () => { -+ try { -+ const result = { id, ok: true, page: await browsers.read(botId) }; -+ browsers.noteRead(botId); -+ return result; -+ } catch (error) { -+ return { id, ok: false, error: error instanceof Error ? error.message : String(error) }; -+ } -+ })(); -+ answered.set(id, job); -+ if (answered.size > 200) answered.delete(answered.keys().next().value); -+ } -+ await post("browser-result", await job).catch((error) => console.warn("[ds-bot] browser result", error)); -+ }; -+ const loop = async () => { -+ let failures = 0; -+ while (!stopped) { -+ poll = new AbortController(); -+ const open = browsers.openPages(); -+ reported = JSON.stringify(open); -+ try { -+ const requests = await post("browser-wait", { clientId, open }, poll.signal); -+ failures = 0; -+ for (const request of Array.isArray(requests) ? requests : []) void answer(request); -+ } catch { -+ if (stopped) return; -+ if (poll.signal.aborted) continue; -+ failures += 1; -+ await sleep(Math.min(3e4, retryMs * 2 ** Math.min(failures, 5))); -+ } -+ } -+ }; -+ const offPages = browsers.pages.subscribe(() => { -+ if (JSON.stringify(browsers.openPages()) !== reported) poll?.abort(); -+ }); -+ void loop(); -+ return () => { -+ stopped = true; -+ offPages(); -+ poll?.abort(); -+ }; -+} -+ -+// src/client/mobile.js -+var MOBILE_MAX_WIDTH = 640; -+var MOBILE_CSS = ` -+@media (max-width:${MOBILE_MAX_WIDTH}px){ -+.bt-held-actions{max-width:100%} -+.bt-held-card{padding:10px 12px} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default} -+.bt-held-retry{min-height:32px} -+.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))} -+.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)} -+.bt-flybot-pointer{max-width:100%;min-width:0} -+.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none} -+.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} -+.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px} -+.bt-inbox-card{flex-wrap:wrap;row-gap:8px} -+.bt-inbox-copy{flex-basis:calc(100% - 38px)} -+/* In progress and Done only. A Needs you ask is the whole text: inbox.js unsets this clamp on those five statuses (#1177). */ -+.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2} -+.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px} -+.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px} -+} -+`; -+ -+// src/client/panes.js -+var PANE_MIN_WIDTH_CSS = ` -+.bt-panel{min-width:0} -+.bt-panel-view{min-width:0} -+.bt-drawer-body{min-width:0;overflow-wrap:anywhere} -+.bt-drawer-body :is(.bt-muted,.bt-note,.bt-section-title,.bt-drawer-role,.bt-drawer-permission){min-width:0;overflow-wrap:anywhere} -+.bt-panel :is(.bt-member,.bt-members,.bt-bcard-row,.bt-drawer-model,.bt-usage-row){min-width:0} -+.bt-panel :is(.bt-muted,.bt-note){white-space:normal} -+[class*="_centerCol"]{min-width:0} -+html,body,#root{overflow-x:clip} -+[class*="_frame"]>[class*="_centerCol"]{overflow-x:clip} -+[class*="_frame"]:has(> [class*="_centerCol"]){overflow-x:clip} -+`; -+var COMPOSER = "[data-composer-input], [data-composer-card]"; -+function clipScroll(el) { -+ if (el && el.scrollLeft > 0) el.scrollLeft = 0; -+} -+function appFrameOf(doc) { -+ if (typeof doc.querySelector !== "function") return null; -+ const col = doc.querySelector('[class*="_centerCol"]'); -+ const frame = col?.parentElement; -+ if (!frame) return null; -+ const name = frame.className; -+ const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; -+ return text2.includes("_frame") ? frame : null; -+} -+function levelShell(doc, from) { -+ const page = doc.scrollingElement || doc.documentElement; -+ clipScroll(page); -+ clipScroll(doc.documentElement); -+ clipScroll(doc.body); -+ clipScroll(appFrameOf(doc)); -+ for (let at = from?.parentElement || null; at; at = at.parentElement) clipScroll(at); -+} -+function composerOf(node) { -+ let el = node; -+ if (el && el.nodeType !== 1) el = el.parentElement; -+ if (!el || typeof el.closest !== "function") return null; -+ return el.closest(COMPOSER); -+} -+function watchShellLevel(doc) { -+ const later = (from) => { -+ const reset = () => levelShell(doc, from); -+ reset(); -+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); -+ }; -+ const onFocus = (event) => later(composerOf(event.target) || void 0); -+ const onInput = (event) => { -+ const composer = composerOf(event.target); -+ if (composer) later(composer); + function BotsPage({ roster, actions, run }) { +- const [creating, setCreating] = (0, import_react28.useState)(false); ++ const [creating, setCreating] = (0, import_react42.useState)(false); + return [ +- (0, import_react28.createElement)(MainBotsSection, { key: "main", roster, actions, run }), +- (0, import_react28.createElement)(MotionSection, { key: "motion", roster, actions, run }), +- creating ? (0, import_react28.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react28.createElement)( ++ (0, import_react42.createElement)(MainBotsSection, { key: "main", roster, actions, run }), ++ (0, import_react42.createElement)(MotionSection, { key: "motion", roster, actions, run }), ++ creating ? (0, import_react42.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react42.createElement)( + "div", + { key: "new", className: "bt-actions" }, +- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("New Bot")) ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react42.createElement)(PlusIcon), t("New Bot")) + ), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetSection, +- { key: "bots", title: t("Bots · {count}", { count: roster.bots.length }) }, +- roster.bots.map((bot) => (0, import_react28.createElement)( ++ { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, ++ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react42.createElement)( + "button", + { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "span", + { className: "bt-set-bot" }, +- (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), +- (0, import_react28.createElement)("span", { className: "bt-row-title" }, bot.name), +- bot.role ? (0, import_react28.createElement)("span", { className: "bt-tag" }, bot.role) : null, +- isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, +- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ++ (0, import_react42.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), ++ (0, import_react42.createElement)("span", { className: "bt-row-title" }, bot.name), ++ bot.role ? (0, import_react42.createElement)("span", { className: "bt-tag" }, bot.role) : null, ++ isMainOf(roster, bot.id) ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, ++ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null + ), +- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) ++ (0, import_react42.createElement)("span", { className: "bt-chevron" }, (0, import_react42.createElement)(ChevronRightIcon)) + )) + ) + ]; + } + function GroupsPage({ roster, actions, run }) { +- const [creating, setCreating] = (0, import_react28.useState)(false); ++ const [creating, setCreating] = (0, import_react42.useState)(false); + return [ +- creating ? (0, import_react28.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react28.createElement)( ++ creating ? (0, import_react42.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react42.createElement)( + "div", + { key: "new", className: "bt-actions" }, +- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("Create group chat")) ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react42.createElement)(PlusIcon), t("Create group chat")) + ), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + SetSection, + { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, +- roster.rooms.length === 0 ? (0, import_react28.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { ++ roster.rooms.length === 0 ? (0, import_react42.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { + const admin = roster.byId[room2.admin]; +- return (0, import_react28.createElement)( ++ return (0, import_react42.createElement)( + "button", + { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "span", + { className: "bt-set-bot" }, +- (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react28.createElement)("span", { className: "bt-row-title" }, room2.name), +- (0, import_react28.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })), +- admin ? (0, import_react28.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react28.createElement)(AdminStar), admin.name) : null, +- room2.hidden ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ++ (0, import_react42.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), ++ (0, import_react42.createElement)("span", { className: "bt-row-title" }, room2.name), ++ (0, import_react42.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), ++ admin ? (0, import_react42.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react42.createElement)(AdminStar), admin.name) : null, ++ room2.hidden ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null + ), +- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) ++ (0, import_react42.createElement)("span", { className: "bt-chevron" }, (0, import_react42.createElement)(ChevronRightIcon)) + ); + }) + ) + ]; + } + function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { +- const [error, setError] = (0, import_react28.useState)(""); +- const close = (0, import_react28.useCallback)(() => actions.closeSettings(), []); ++ const [error, setError] = (0, import_react42.useState)(""); ++ const close = (0, import_react42.useCallback)(() => actions.closeSettings(), []); + useEscape(close); + const run = (promise) => Promise.resolve(promise).then(() => { + setError(""); +@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false + }); + const bot = page === "bots" && id ? roster.byId[id] : void 0; + const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; +- const shown = SETTINGS_PAGES.some(([key]) => key === page) ? page : "general"; ++ const extra = useSettingsPages().map((entry) => ({ entry, key: `plugin:${entry.id}` })); ++ const pages = SETTINGS_PAGES.flatMap((item) => item[0] === "connectors" ? [item, ...extra.map(({ entry, key }) => [key, entry.label ?? entry.id])] : [item]); ++ const shown = pages.some(([key]) => key === page) ? page : "general"; ++ const pluginPage = extra.find(({ key }) => key === shown)?.entry; + const usageFocus = shown === "usage" && id ? id : null; +- const label = t(SETTINGS_PAGES.find(([key]) => key === shown)[1]); ++ const label = t(pages.find(([key]) => key === shown)[1]); + const back = bot ? t("Back to Bots") : room2 ? t("Back to group chats") : usageFocus ? t("All usage") : null; + const nav = useGlide(shown, { axis: "y" }); + const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; + const depth = bot || room2 ? 1 : 0; +- const last = (0, import_react28.useRef)({ key: pageKey, depth, move: "up" }); ++ const last = (0, import_react42.useRef)({ key: pageKey, depth, move: "up" }); + if (last.current.key !== pageKey) { + last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; + } +- const [usageOpened, setUsageOpened] = (0, import_react28.useState)(shown === "usage"); ++ const [usageOpened, setUsageOpened] = (0, import_react42.useState)(shown === "usage"); + if (shown === "usage" && !usageOpened) setUsageOpened(true); +- const errorLine = error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; +- return (0, import_react28.createElement)( ++ const errorLine = error ? (0, import_react42.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; ++ return (0, import_react42.createElement)( + "div", + { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, +- (0, import_react28.createElement)("div", { className: "bt-scrim", onMouseDown: close }), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)("div", { className: "bt-scrim", onMouseDown: close }), ++ (0, import_react42.createElement)( + "div", +- { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("DS Bot settings"), "data-wide": shown === "usage" ? "" : void 0 }, +- (0, import_react28.createElement)( ++ { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, ++ (0, import_react42.createElement)( + "nav", + { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, +- (0, import_react28.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), +- SETTINGS_PAGES.map(([key, text]) => (0, import_react28.createElement)("button", { ++ (0, import_react42.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), ++ pages.map(([key, text2]) => (0, import_react42.createElement)("button", { + key, + type: "button", + className: "bt-settings-item", + "aria-current": shown === key ? "page" : void 0, + onClick: () => actions.openSettings(key) +- }, (0, import_react28.createElement)(PAGE_ICONS[key]), t(text))), +- (0, import_react28.createElement)("button", { ++ }, (0, import_react42.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))), ++ // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot ++ // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). ++ (0, import_react42.createElement)("button", { + type: "button", + className: "bt-settings-item bt-settings-shell", +- title: t("Models, providers, and the rest of DeepSeek Harness"), ++ title: t("Opens DeepSeek Harness's own settings (models, providers, permissions) in a separate window"), ++ "aria-label": t("Harness settings, opens DeepSeek Harness's own settings window"), + onClick: () => { + close(); + openHarnessSettings(); + } +- }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings")) ++ }, (0, import_react42.createElement)(SlidersIcon), t("Harness settings"), (0, import_react42.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) + ), +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "div", + { className: "bt-settings-main" }, +- (0, import_react28.createElement)( ++ (0, import_react42.createElement)( + "div", + { className: "bt-settings-head" }, +- back ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react28.createElement)(ChevronLeftIcon)) : null, +- (0, import_react28.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) ++ back ? (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react42.createElement)(ChevronLeftIcon)) : null, ++ (0, import_react42.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) + ), +- shown === "usage" ? null : (0, import_react28.createElement)( ++ shown === "usage" ? null : (0, import_react42.createElement)( + "div", + { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, + errorLine, +- shown === "general" ? (0, import_react28.createElement)(GeneralPage, { roster, actions, run }) : null, +- bot ? (0, import_react28.createElement)("div", { className: "bt-settings-form" }, (0, import_react28.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, +- shown === "bots" && !bot ? (0, import_react28.createElement)(BotsPage, { roster, actions, run }) : null, +- room2 ? (0, import_react28.createElement)("div", { className: "bt-settings-form" }, (0, import_react28.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, +- shown === "groups" && !room2 ? (0, import_react28.createElement)(GroupsPage, { roster, actions, run }) : null, +- shown === "connectors" ? (0, import_react28.createElement)(ConnectorsPage, { roster, actions }) : null, +- shown === "capabilities" ? (0, import_react28.createElement)(CapabilitiesPage, { roster, actions, run }) : null, +- shown === "schedules" ? (0, import_react28.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, +- shown === "secrets" ? (0, import_react28.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, +- shown === "feedback" ? (0, import_react28.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, +- shown === "about" ? (0, import_react28.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null ++ shown === "general" ? (0, import_react42.createElement)(GeneralPage, { roster, actions, run }) : null, ++ bot ? (0, import_react42.createElement)("div", { className: "bt-settings-form" }, (0, import_react42.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, ++ shown === "bots" && !bot ? (0, import_react42.createElement)(BotsPage, { roster, actions, run }) : null, ++ room2 ? (0, import_react42.createElement)("div", { className: "bt-settings-form" }, (0, import_react42.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, ++ shown === "groups" && !room2 ? (0, import_react42.createElement)(GroupsPage, { roster, actions, run }) : null, ++ shown === "projects" ? (0, import_react42.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null, ++ shown === "connectors" ? (0, import_react42.createElement)(ConnectorsPage, { roster, actions }) : null, ++ shown === "workflows" ? (0, import_react42.createElement)(WorkflowsPage, { call: call2, roster }) : null, ++ pluginPage ? (0, import_react42.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, ++ shown === "capabilities" ? (0, import_react42.createElement)(CapabilitiesPage, { roster, actions, run }) : null, ++ shown === "schedules" ? (0, import_react42.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, ++ shown === "secrets" ? (0, import_react42.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, ++ shown === "feedback" ? (0, import_react42.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, ++ shown === "about" ? (0, import_react42.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null + ), +- usageOpened ? (0, import_react28.createElement)( ++ usageOpened ? (0, import_react42.createElement)( + "div", + { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, + shown === "usage" ? errorLine : null, +- (0, import_react28.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) ++ (0, import_react42.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) + ) : null + ), +- (0, import_react28.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react28.createElement)(CloseIcon)) ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react42.createElement)(CloseIcon)) + ) + ); + } ++var openProjectBoard = (id) => { ++ if (typeof window !== "undefined") window.location.hash = `#project=${id}`; ++}; ++var projectMembers = (roster) => [ ++ ...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })), ++ ...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null })) ++]; ++function ProjectsList({ projects, roster, call: call4, reload }) { ++ const members = projectMembers(roster); ++ const bind = async (choice, project) => { ++ const [kind, id] = String(choice ?? "").split(":"); ++ if (!kind || !id) return; ++ await call4("project-bind", { kind, id, projectId: project.id }); ++ await reload?.(); + }; -+ const onScroll = (event) => { -+ const target = event.target; -+ if (target === doc || target === doc.documentElement || target === doc.body) levelShell(doc); ++ const unbind = async (member) => { ++ await call4("project-bind", { kind: member.kind, id: member.id, projectId: null }); ++ await reload?.(); + }; -+ doc.addEventListener("focusin", onFocus); -+ doc.addEventListener("input", onInput); -+ doc.addEventListener("scroll", onScroll, true); -+ return () => { -+ doc.removeEventListener("focusin", onFocus); -+ doc.removeEventListener("input", onInput); -+ doc.removeEventListener("scroll", onScroll, true); ++ const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0); ++ return [ ++ (0, import_react42.createElement)( ++ SetSection, ++ { key: "projects", title: t("Projects · {count}", { count: projects.length }) }, ++ projects.length === 0 ? (0, import_react42.createElement)("div", { className: "bt-set-row" }, (0, import_react42.createElement)("div", { className: "bt-set-copy" }, (0, import_react42.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react42.createElement)( ++ SetRow, ++ { ++ key: project.id, ++ label: project.name, ++ hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir ++ }, ++ (0, import_react42.createElement)( ++ "div", ++ { className: "bt-project-row-controls" }, ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")), ++ (0, import_react42.createElement)( ++ "select", ++ { ++ className: "bt-select", ++ "aria-label": t("Bind to {name}", { name: project.name }), ++ value: "", ++ onChange: (event) => { ++ void bind(event.target.value, project); ++ } ++ }, ++ (0, import_react42.createElement)("option", { value: "" }, t("Bind a Bot or group")), ++ members.map((member) => (0, import_react42.createElement)( ++ "option", ++ { key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` }, ++ `${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}` ++ )) ++ ) ++ ) ++ )) ++ ), ++ bound2.length === 0 ? null : (0, import_react42.createElement)( ++ SetSection, ++ { key: "bindings", title: t("Bindings") }, ++ bound2.map((member) => (0, import_react42.createElement)( ++ SetRow, ++ { ++ key: `${member.kind}:${member.id}`, ++ label: member.name, ++ hint: member.kind === "group" ? t("Group") : t("Bot") ++ }, ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { ++ void unbind(member); ++ } }, t("Unbind")) ++ )) ++ ) ++ ]; ++} ++function ProjectsPage({ roster, actions, run, call: call4 }) { ++ const [state, setState] = (0, import_react42.useState)({ projects: [], error: null }); ++ const [name, setName] = (0, import_react42.useState)(""); ++ const [workDir, setWorkDir] = (0, import_react42.useState)(null); ++ const reload = (0, import_react42.useCallback)(async () => { ++ const listing = await call4("projects"); ++ setState({ projects: listing?.projects ?? [], error: null }); ++ }, [call4]); ++ (0, import_react42.useEffect)(() => { ++ void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) })); ++ }, [reload]); ++ const create = async () => { ++ const trimmed3 = name.trim(); ++ if (trimmed3 === "") return; ++ const body = { name: trimmed3 }; ++ if (workDir !== null && workDir !== "") body.workDir = workDir; ++ try { ++ await call4("project-create", body); ++ setName(""); ++ setWorkDir(null); ++ await reload(); ++ } catch (error) { ++ setState({ projects: state.projects, error: error?.message ?? String(error) }); ++ } + }; ++ return [ ++ state.error === null ? null : (0, import_react42.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error), ++ (0, import_react42.createElement)( ++ SetSection, ++ { key: "new", title: t("New Project") }, ++ (0, import_react42.createElement)( ++ SetRow, ++ { label: t("Name") }, ++ (0, import_react42.createElement)("input", { ++ className: "bt-input", ++ "aria-label": t("Project name"), ++ value: name, ++ placeholder: t("e.g. Website"), ++ onChange: (event) => setName(event.target.value) ++ }) ++ ), ++ (0, import_react42.createElement)( ++ SetRow, ++ { label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") }, ++ (0, import_react42.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir }) ++ ), ++ (0, import_react42.createElement)( ++ "div", ++ { className: "bt-actions" }, ++ (0, import_react42.createElement)("button", { ++ type: "button", ++ className: "bt-primary bt-with-icon", ++ disabled: name.trim() === "", ++ onClick: () => { ++ void create(); ++ } ++ }, (0, import_react42.createElement)(PlusIcon), t("New Project")) ++ ) ++ ), ++ // The list is rendered here rather than as a child element: it holds no state of its own, ++ // and the page keeps one reload path for both halves. ++ ...ProjectsList({ projects: state.projects, roster, call: call4, reload }) ++ ]; ++} + + // src/client/context-menu.js +-var import_react29 = require("react"); ++var import_react43 = require("react"); + function ContextMenu({ menu, roster, actions, leaving = false }) { + useEscape(() => actions.closeMenu()); +- (0, import_react29.useEffect)(() => { ++ (0, import_react43.useEffect)(() => { + const onDown = (event) => { + if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); + }; + window.addEventListener("mousedown", onDown); + return () => window.removeEventListener("mousedown", onDown); + }, []); +- const ref = (0, import_react29.useRef)(null); +- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); +- (0, import_react29.useLayoutEffect)(() => { ++ const structure = useSidebarStructure(); ++ const ref = (0, import_react43.useRef)(null); ++ const [place, setPlace] = (0, import_react43.useState)({ left: menu.x, top: menu.y }); ++ (0, import_react43.useLayoutEffect)(() => { + const box = ref.current?.getBoundingClientRect(); + if (!box) return; + setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); + }, [menu.x, menu.y]); +- if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react29.createElement)(AgentMenu, { menu, roster, actions, leaving }); ++ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react43.createElement)(AgentMenu, { menu, roster, actions, leaving }); + const bot = roster.byId[menu.id]; + const room2 = roster.roomsById[menu.id]; + const entry = bot ?? room2; +@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { + const isMainBot = isMainOf(roster, menu.id); + const isTile = roster.mainBotId === menu.id; + const mainCount = (roster.mainBotIds ?? []).length; +- const groups = [ ++ const groups = bot?.remote ? [ ++ [ ++ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] ++ ], ++ [ ++ [t("Bot settings"), GearIcon, () => actions.openSettings("bots", bot.id)] ++ ], ++ pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]), ++ [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(currentPart(bot))]] ++ ] : [ + [ + isTile ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })], + [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] + ], + [ +- bot ? [t("Rename Bot"), PencilIcon, () => actions.renameBot(bot.id)] : null, ++ // One way to edit a Bot (#385): its name is edited in Edit Bot too. + bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, + room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, + room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, +@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { + bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, + bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null + ], ++ bot ? pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]) : [], + [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(bot ? currentPart(bot) : menu.id)]], + [ + isMainBot ? null : [t("Hide from sidebar"), HideIcon, () => actions.setFlags(menu.id, { hidden: true })], + isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true] + ] +- ].map((group) => group.filter(Boolean)).filter((group) => group.length > 0); +- return (0, import_react29.createElement)( ++ ]; ++ const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); ++ return (0, import_react43.createElement)( + "div", + { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, +- groups.map((group, index) => (0, import_react29.createElement)( +- import_react29.Fragment, ++ shown.map((group, index) => (0, import_react43.createElement)( ++ import_react43.Fragment, + { key: index }, +- index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { ++ index > 0 ? (0, import_react43.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, danger]) => (0, import_react43.createElement)("button", { + key: label, + type: "button", + role: "menuitem", +@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { + actions.closeMenu(); + void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); + } +- }, (0, import_react29.createElement)(Icon), label)) ++ }, (0, import_react43.createElement)(Icon), label)) + )) + ); + } + function AgentMenu({ menu, roster, actions, leaving }) { +- const ref = (0, import_react29.useRef)(null); +- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); +- (0, import_react29.useLayoutEffect)(() => { ++ const ref = (0, import_react43.useRef)(null); ++ const [place, setPlace] = (0, import_react43.useState)({ left: menu.x, top: menu.y }); ++ (0, import_react43.useLayoutEffect)(() => { + const box = ref.current?.getBoundingClientRect(); + if (!box) return; + setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); +@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { + [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] + ] + ]; +- return (0, import_react29.createElement)( ++ return (0, import_react43.createElement)( + "div", + { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, +- groups.map((group, index) => (0, import_react29.createElement)( +- import_react29.Fragment, ++ groups.map((group, index) => (0, import_react43.createElement)( ++ import_react43.Fragment, + { key: index }, +- index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { ++ index > 0 ? (0, import_react43.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, danger]) => (0, import_react43.createElement)("button", { + key: label, + type: "button", + role: "menuitem", +@@ -10968,16 +10968,31 @@ function AgentMenu({ menu, roster, actions, leaving }) { + actions.closeMenu(); + void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); + } +- }, (0, import_react29.createElement)(Icon), label)) ++ }, (0, import_react43.createElement)(Icon), label)) + )) + ); + } + ++// src/client/pane-width.js ++var DEFAULT_WIDTH = 520; ++var DETAILS_WIDTH = 380; ++var DETAILS_MAX = 520; ++var MIN_WIDTH = 360; ++var MIN_CHAT = 420; ++var MIN_DETAILS = 240; ++function panelWidthRange(view, viewport) { ++ const room2 = Math.max(0, viewport - MIN_CHAT); ++ if (view === "browser") return [MIN_WIDTH, Math.max(MIN_WIDTH, room2)]; ++ const low = Math.max(MIN_DETAILS, Math.min(DETAILS_WIDTH, room2)); ++ return [low, Math.max(low, Math.min(DETAILS_MAX, room2))]; +} + -+// src/client/project-board.js -+var import_react47 = require("react"); -+var PROJECT_BOARD_CSS = ` -+/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the -+ bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the -+ Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */ -+.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} -+.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))} -+.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto} -+.bt-project-board{padding:16px 18px} -+.bt-project-group{margin:0 0 14px} -+.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} -+.bt-project-row[data-column="done"]{opacity:.7} -+.bt-project-title-wrap{display:flex;flex-direction:column;gap:2px;min-width:0} -+.bt-project-audit{font-size:11px;line-height:1.45;white-space:pre-wrap;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} -+.bt-project-empty{padding:22px 4px;opacity:.7} -+/* #1767: the record beside the cards. The same surface as a row, one step quieter, and no new -+ component language: a heading, a list, and a monospace hash the reader can compare. */ -+.bt-project-record{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} -+.bt-project-record h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-record h5{margin:10px 0 4px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;opacity:.55} -+.bt-project-decision,.bt-project-meeting{display:flex;gap:10px;align-items:baseline;padding:6px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} -+.bt-project-decision .bt-project-date,.bt-project-meeting .bt-project-date{font-size:11px;opacity:.6;min-width:76px;font-variant-numeric:tabular-nums} -+.bt-project-decision .bt-project-hash{margin-left:auto;font-size:11px;opacity:.55;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-decision .bt-project-title,.bt-project-meeting .bt-project-title{min-width:0} -+.bt-project-path{font-size:11px;opacity:.6;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-missing{font-size:11px;opacity:.6} -+.bt-project-status{font-size:11px;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-left:auto} -+/* #1831: the card's level, the people it is addressed to, and the identity a row or a comment -+ carries. A severe card is the only one painted louder, because it is the only one that waits. */ -+.bt-project-level{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));opacity:.75} -+.bt-project-level[data-level="severe"]{opacity:1;border-color:#c2410c;color:#c2410c} -+.bt-project-recipients{font-size:11px;opacity:.65} -+.bt-project-paused{font-size:11px;opacity:.75;color:#c2410c} -+.bt-project-holder{font-size:11px;opacity:.7} -+.bt-project-speaks-for{font-size:11px;opacity:.7;font-style:italic} -+.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} -+.bt-project-metrics{margin:0 0 14px} -+.bt-project-metric-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:8px} -+.bt-project-metric{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border:1px solid rgba(127,127,127,.24);border-radius:8px} -+.bt-project-metric-value{font-size:18px;font-weight:600} -+.bt-project-metric-label{font-size:12px;font-weight:600} -+.bt-project-metric-caption{font-size:10px;opacity:.6;line-height:1.3} -+.bt-project-metrics-note{margin:0 0 6px;font-size:11px;opacity:.7} -+.bt-project-running{margin:0 0 14px} -+.bt-project-running-head,.bt-project-running-row{display:grid;grid-template-columns:2fr 1fr 1fr .8fr .8fr 1.2fr;gap:8px;font-size:12px;padding:3px 0} -+.bt-project-running-head{opacity:.6;font-size:10px;letter-spacing:.03em} -+.bt-project-running-row[role="button"]{cursor:pointer} -+.bt-project-running-row[data-overdue="true"]{color:#c2410c} -+.bt-project-counts{display:flex;gap:14px;align-items:center;margin:0 0 12px;font-size:12px;letter-spacing:.03em;opacity:.8} -+.bt-project-counts b{font-weight:600} -+.bt-project-group[data-queue="waiting"] .bt-project-row{cursor:default} -+.bt-project-role{font-size:11px;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-wait{font-size:11px;opacity:.65;margin-left:auto} -+.bt-project-state{font-size:11px;margin-left:auto} -+.bt-project-row[data-state="stalled"]{border-left:2px solid #d9822b} -+.bt-project-reason{font-size:11px;opacity:.7} -+ -+/* #1855: the finished cards are one group of their own, closed until the reader opens it — a -+ Project with months of history reads as the work in flight, not as its archive. */ -+.bt-project-done{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} -+.bt-project-done>summary{cursor:pointer;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7;list-style:none} -+.bt-project-done>summary::-webkit-details-marker{display:none} -+.bt-project-done>summary::before{content:'\\25B8';display:inline-block;width:12px;opacity:.6} -+.bt-project-done[open]>summary::before{content:'\\25BE'} -+.bt-project-done[open]>summary{margin:0 0 8px} -+ -+/* #1749: management panels. Same surface as the board, one form row per control, and a status -+ line that quotes the host's own answer — a refused save reads where the reader pressed it. */ -+.bt-project-panel{padding:16px 18px;display:flex;flex-direction:column;gap:12px} -+.bt-project-panel h3{margin:0} -+.bt-project-field{display:flex;flex-direction:column;gap:4px} -+.bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} -+.bt-project-field textarea{min-height:88px;resize:vertical} -+.bt-project-row-actions{display:flex;gap:8px;align-items:center} -+.bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} -+.bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} -+.bt-project-member .bt-project-member-kind{margin-left:auto;font-size:11px;opacity:.6} -+.bt-project-status-line{font-size:12px;opacity:.75;min-height:16px} -+.bt-project-note{font-size:12px;opacity:.65;margin:0} -+ -+/* #1860: the Project team — who runs the Project, who speaks for the reader, and the role pool. -+ One quiet block above the cards, and a Bot row is a button because a reader is looking a -+ person up. The pool row keeps the rank it plays and the ranks the host said it may switch to. */ -+.bt-project-team{margin:0 0 14px} -+.bt-project-team h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-team-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} -+.bt-project-team-row[data-open-bot]{cursor:pointer} -+.bt-project-team-row[data-open-bot]:hover{background:var(--bt-hover,rgba(127,127,127,.14))} -+.bt-project-team-label{font-size:12px;opacity:.7;min-width:96px} -+.bt-project-team-bot{font-size:12px;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-team-roles{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.75} -+.bt-project-team-role{font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-team-myrole{margin-left:auto;font-size:11px;opacity:.65} -+`; -+var PROJECT_SOURCES = ["bot", "group", "workflow"]; -+var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; -+var text = (value) => typeof value === "string" ? value.trim() : ""; -+var DONE_COLUMN = "done"; -+var boardOfRow = (row, task) => { -+ if (task === void 0) return null; -+ if (row.source === "group") { -+ const id = text(task.origin?.roomId); -+ return id === "" ? null : { kind: "group", id }; -+ } -+ if (row.source === "bot") { -+ const id = text(task.ownerBotId); -+ return id === "" ? null : { kind: "bot", id }; + // src/client/storage.js + var STORAGE = { + unread: "ds-bot:unread", + reactions: "ds-bot:reactions", + seen: "ds-bot:seen", ++ friendLines: "ds-bot:friend-lines", + prefs: "ds-bot:prefs", + surface: "ds-bot:surface", + agentSessions: "ds-bot:agent-sessions", +@@ -11026,256 +11026,20 @@ function migrateStorage(storage = globalThis.localStorage) { + } + } + try { +- migrateBrowsers(storage); +- } catch { +- } +- try { +- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); +- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { +- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); +- } +- } catch { +- } +-} +- +-// src/client/member-status.js +-var import_react30 = require("react"); +-var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; +-var ROWS_MAX = 4; +-function memberWork(room2, work = NO_WORK) { +- const here = work.rooms?.[room2.id] ?? []; +- return room2.members.map((id) => { +- const approvals = (work.approvals ?? []).filter((entry) => entry.botId === id); +- const local = here.find((entry) => entry.botId === id); +- const doing = local ? { ...local, roomId: room2.id } : work.bots?.[id]; +- return { id, approvals, doing, here: local !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; +- }); +-} +-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] })); +-} +-function splitBusy(busy) { +- const speaking = (entry) => entry.state === "work" && entry.here && entry.doing.speaking === true; +- const rows = busy.filter((entry) => entry.state === "work" && entry.here && !speaking(entry)).slice(0, ROWS_MAX); +- return { rows, rest: busy.filter((entry) => !rows.includes(entry) && !speaking(entry)) }; +-} +-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, since: entry.since }] : []; +- }); +-} +-function summaryText(busy) { +- const asks = busy.filter((entry) => entry.state === "ask"); +- const workers = busy.filter((entry) => entry.state === "work"); +- const names = asks.slice(0, 2).map((entry) => entry.bot.name); +- const waiting = asks.length === 0 ? "" : asks.length === 1 ? t("{name} is waiting for your approval", { name: names[0] }) : asks.length === 2 ? t("{first} and {second} are waiting for your approval", { first: names[0], second: names[1] }) : t("{first}, {second} and {count} more are waiting for your approval", { first: names[0], second: names[1], count: asks.length - 2, total: asks.length }); +- const first = workers[0]; +- const doing = first ? `${first.bot.name} · ${workPhrase(first.doing.tool)}…${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; +- return { waiting, doing }; +-} +-function Face({ entry, size }) { +- return (0, import_react30.createElement)( +- "span", +- { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, +- (0, import_react30.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) +- ); +-} +-function WorkRow({ bot, tool, label, at, now, onOpen }) { +- const text = `${label} · ${workPhrase(tool)}…`; +- const elapsed = typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +- return (0, import_react30.createElement)( +- "button", +- { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, +- (0, import_react30.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }), +- (0, import_react30.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react30.createElement)(Shimmer, null, text)), +- elapsed ? (0, import_react30.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null +- ); +-} +-function SummaryLine({ busy, onOpen }) { +- const { waiting, doing } = summaryText(busy); +- return (0, import_react30.createElement)( +- "div", +- { className: "bt-event bt-mw-dock" }, +- (0, import_react30.createElement)( +- "button", +- { type: "button", className: "bt-event-whole bt-mw-line", "aria-label": [waiting, doing].filter(Boolean).join(" · "), onClick: onOpen }, +- (0, import_react30.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react30.createElement)(Face, { key: entry.id, entry, size: 16 }))), +- waiting ? (0, import_react30.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, +- waiting && doing ? (0, import_react30.createElement)("span", { "aria-hidden": true }, "·") : null, +- doing ? (0, import_react30.createElement)("span", { className: "bt-event-name" }, (0, import_react30.createElement)(Shimmer, null, doing)) : null +- ) +- ); +-} +-function MemberDock({ sessionId, useRoster, useWork, actions }) { +- const roster = useRoster((value) => value); +- const work = useWork((value) => value); +- const room2 = roster.roomsById[sessionId]; +- const bot = room2 ? void 0 : roster.byId[sessionId]; +- const ownChat = bot !== void 0 && work.bots?.[bot.id] !== void 0 && work.bots[bot.id].roomId === void 0; +- const busy = room2 ? busyMembers(room2, work, roster) : []; +- const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : []; +- const shown = busy.length > 0 || groups.length > 0; +- const now = useNow2(shown); +- const ref = (0, import_react30.useRef)(null); +- useFlowColumn(ref, shown, sessionId); +- if (!shown) return null; +- if (room2) { +- const { rows, rest } = splitBusy(busy); +- const open = () => actions.showDetails(sessionId); +- return (0, import_react30.createElement)( +- "div", +- { ref, className: "bt-mw-dock-stack" }, +- rows.map((entry) => (0, import_react30.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })), +- rest.length > 0 ? (0, import_react30.createElement)(SummaryLine, { busy: rest, onOpen: open }) : null +- ); +- } +- return (0, import_react30.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react30.createElement)(WorkRow, { +- key: item.room.id, +- bot, +- tool: item.tool, +- label: t("in {name}", { name: item.room.name }), +- at: item.since, +- now, +- onOpen: () => actions.openSession(item.room.id) +- }))); +-} +-function useFlowColumn(ref, active, key) { +- (0, import_react30.useLayoutEffect)(() => { +- const node = ref.current; +- const flow = document.querySelector("[data-chat-flow]"); +- if (!active || !node || !flow) return void 0; +- const place = () => { +- node.style.marginInlineStart = "0px"; +- const column = flow.getBoundingClientRect(); +- node.style.marginInlineStart = `${Math.round(column.left - node.getBoundingClientRect().left)}px`; +- node.style.width = `${Math.round(column.width)}px`; +- }; +- place(); +- const observer = new ResizeObserver(place); +- for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); +- return () => observer.disconnect(); +- }, [active, key]); +-} +-function useNow2(active) { +- const [now, setNow] = (0, import_react30.useState)(Date.now); +- (0, import_react30.useEffect)(() => { +- if (!active) return void 0; +- const timer = setInterval(() => setNow(Date.now()), 3e4); +- return () => clearInterval(timer); +- }, [active]); +- return now; +-} +-var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; +-function ApprovalCard({ approval, bot, actions }) { +- const [busy, setBusy] = (0, import_react30.useState)(false); +- const answer = (choice) => { +- setBusy(true); +- void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); +- }; +- return (0, import_react30.createElement)( +- "div", +- { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, +- (0, import_react30.createElement)("div", { className: "bt-mw-tool" }, approval.tool), +- approval.detail?.text ? (0, import_react30.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, +- approval.reason ? (0, import_react30.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null, +- approval.detail?.code ? (0, import_react30.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null, +- approval.answerable ? (0, import_react30.createElement)( +- "div", +- { className: "bt-mw-actions" }, +- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")), +- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")) +- ) : (0, import_react30.createElement)( +- "div", +- { className: "bt-mw-actions" }, +- (0, import_react30.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) +- ) +- ); +-} +-function MemberWork({ entry, bot, room: room2, roster, actions, now }) { +- const [stopping, setStopping] = (0, import_react30.useState)(false); +- if (entry.state === "ask") { +- return (0, import_react30.createElement)( +- "div", +- { className: "bt-mw-member" }, +- (0, import_react30.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_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) +- ); ++ migrateBrowsers(storage); ++ } catch { + } -+ return null; -+}; -+var decisionOf = (file) => { -+ if (file === null || typeof file !== "object") return null; -+ const path = text(file.path); -+ if (path === "") return null; -+ return { -+ key: `decision:${path}`, -+ path, -+ name: text(file.name) || path.slice(path.lastIndexOf("/") + 1), -+ date: text(file.date), -+ title: text(file.title), -+ sha256: text(file.sha256), -+ shortHash: text(file.sha256).slice(0, 12) -+ }; -+}; -+var meetingOf = (meeting, status) => { -+ if (meeting === null || typeof meeting !== "object") return null; -+ const id = text(meeting.id); -+ if (id === "") return null; -+ return { -+ key: `meeting:${id}`, -+ id, -+ date: text(meeting.date), -+ question: text(meeting.question) || text(meeting.title), -+ owner: text(meeting.owner) || null, -+ status, -+ path: text(meeting.path), -+ blockingQuestion: text(meeting.blockingQuestion) || null, -+ // #1831: the level and recipients the host answered, and the refusal to advance a severe card. -+ // Nothing is inferred here: a record written before the slice carries an ordinary card and no -+ // recipients, exactly as the host reads it. -+ level: text(meeting.level) || "ordinary", -+ recipients: Array.isArray(meeting.recipients) ? meeting.recipients.map((item) => text(item)).filter((name) => name !== "") : [], -+ advance: meeting.advance !== null && typeof meeting.advance === "object" ? { allowed: meeting.advance.allowed === true, reason: text(meeting.advance.reason) || null } : { allowed: true, reason: null }, -+ // #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 !== "") : [] -+ }; -+}; -+var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; -+function projectBoardRecord({ decisions, meetings } = {}) { -+ const dir = text(decisions?.dir) || null; -+ const files = listOf(decisions?.files); -+ const decided = Array.isArray(meetings?.decided) ? meetings.decided.map((item) => meetingOf(item, "decided")).filter((item) => item !== null) : []; -+ const pending = Array.isArray(meetings?.pending) ? meetings.pending.map((item) => meetingOf(item, item?.status ?? "deliberating")).filter((item) => item !== null) : []; -+ return { -+ decisions: { -+ dir, -+ files, -+ count: files.length, -+ empty: files.length === 0, -+ emptyText: text(decisions?.reason) || (dir === null ? "This Project names no working folder." : `No decision file in ${dir}/decisions.`) -+ }, -+ meetings: { -+ decided, -+ pending, -+ count: decided.length + pending.length, -+ empty: decided.length === 0 && pending.length === 0, -+ emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." ++ try { ++ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); ++ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { ++ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); + } -+ }; ++ } catch { + } +- if (entry.state !== "work") return null; +- const where = entry.doing.roomId === room2.id ? "" : entry.doing.roomId ? t("in {name}", { name: roster.roomsById[entry.doing.roomId]?.name ?? t("a group chat") }) : t("in its own chat"); +- 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_react30.createElement)( +- "div", +- { className: "bt-mw-member" }, +- (0, import_react30.createElement)( +- "div", +- { className: "bt-mw-state bt-mw-work" }, +- (0, import_react30.createElement)("span", null, (0, import_react30.createElement)(Shimmer, null, `${workPhrase(entry.doing.tool)}…`), where ? ` ${where}` : "", since(now, entry.doing.since)), +- entry.here ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null +- ) +- ); +-} +-function useMemberWork(room2, useWork) { +- const work = useWork?.((value) => value) ?? NO_WORK; +- const entries2 = room2 ? memberWork(room2, work) : []; +- const now = useNow2(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-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} +-.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} +-.bt-mw-row:hover{background:var(--bt-hover)} +-.bt-mw-row>.bt-mark{flex:none} +-.bt-mw-row-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-mw-row-time{flex:none;font-size:12px;color:var(--bt-ink-3);white-space:nowrap} +-.bt-mw-work{display:flex;align-items:center;justify-content:space-between;gap:8px} +-.bt-mw-stop{flex:none;height:22px;padding:0 10px;border:1px solid var(--bt-line,rgba(0,0,0,.12));border-radius:11px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer} +-.bt-mw-stop:hover{background:var(--bt-hover)} +-.bt-mw-stop:disabled{opacity:.5;cursor:default} +-.bt-event.bt-mw-dock{margin:0 0 6px;pointer-events:none} +-.bt-mw-dock>button{pointer-events:auto;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards} +-.bt-mw-line{gap:6px!important} +-.bt-mw-stack{display:inline-flex;align-items:center;flex:none;line-height:0} +-.bt-mw-stack>*+*{margin-left:-4px} +-.bt-mw-face{position:relative;display:inline-flex;flex:none;border-radius:50%} +-.bt-mw-stack .bt-mw-face{box-shadow:0 0 0 1.5px var(--bt-main,#fff)} +-.bt-mw-face[data-ask]::after{content:"";position:absolute;right:-2px;bottom:-2px;width:7px;height:7px;border-radius:50%;background:#e3a008;box-shadow:0 0 0 1.5px var(--bt-main,#fff)} +-.bt-mw-ask{color:#b7791f;flex:none;white-space:nowrap} +-body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a} +-.bt-mw-member{padding:0 6px 8px 42px;display:flex;flex-direction:column;gap:6px} +-.bt-mw-state{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:-4px} +-.bt-mw-card{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border-radius:12px;background:var(--bt-hover);font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-mw-tool{font-weight:600;color:var(--bt-ink)} +-.bt-mw-text{color:var(--bt-ink);overflow-wrap:anywhere} +-.bt-mw-reason{overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +-.bt-mw-code{display:block;font:11px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--bt-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-mw-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px} +-.bt-mw-actions>button{height:28px;padding:0 12px;font-size:12px} +-.bt-mw-actions>button:disabled{opacity:.5;cursor:default} +-@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} +-@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} +-`; + + // src/client/details-drawer.js +-var import_react31 = require("react"); +-function transcriptOf(list, id, name, hiddenTurns = []) { +- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; +- if (!Array.isArray(outline)) return ""; +- const lines = []; +- for (const entry of outline) { +- if (hiddenTurns.includes(entry.turn)) continue; +- if (entry.prompt && !HEADER.test(entry.prompt)) lines.push(`**${t("You")}:** ${entry.prompt.trim()}`); +- if (entry.response) lines.push(`**${name}:** ${stripHeader(entry.response).trim()}`); +- } +- return lines.join("\n\n"); +-} ++var import_react44 = require("react"); + var TAB_LABELS = { details: "Details", browser: "Browser" }; + var hostOf = (url) => { + try { +@@ -11289,10 +11289,10 @@ var EMPTY_TABS = { ids: [], active: null }; + var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; + var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); + function PermissionRow({ bot, roster, actions, run }) { +- const [pending, setPending] = (0, import_react31.useState)(null); ++ const [pending, setPending] = (0, import_react44.useState)(null); + const catalog = roster.permissions; + const saved = bot.permission ?? catalog?.defaultPreset; +- (0, import_react31.useEffect)(() => { ++ (0, import_react44.useEffect)(() => { + if (pending === saved) setPending(null); + }, [pending, saved]); + if (!catalog?.options?.length) return null; +@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) { + setPending(value); + void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); + }; +- return (0, import_react31.createElement)( ++ return (0, import_react44.createElement)( + "div", + { className: "bt-drawer-permission" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Permissions")), ++ (0, import_react44.createElement)( + Segmented, + { label: t("Permissions"), value: current }, +- catalog.options.map((option) => (0, import_react31.createElement)("button", { ++ catalog.options.map((option) => (0, import_react44.createElement)("button", { + key: option.value, + type: "button", + "aria-pressed": option.value === current, +@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) { + function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { + const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); + const pages = useBrowserPages((map) => map); +- (0, import_react31.useEffect)(() => { ++ (0, import_react44.useEffect)(() => { + actions.browserEnsure?.(bot.id); + }, [bot.id]); + if (roster.browser !== true || !actions.browserShow) { +- return (0, import_react31.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); ++ return (0, import_react44.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); + } + if (state.ids.length === 0) { +- return (0, import_react31.createElement)( ++ return (0, import_react44.createElement)( + "div", + { className: "bt-browser-empty-mark" }, +- (0, import_react31.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), +- (0, import_react31.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), +- (0, import_react31.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), +- (0, import_react31.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) ++ (0, import_react44.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), ++ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), ++ (0, import_react44.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) + ); + } +- return (0, import_react31.createElement)( ++ return (0, import_react44.createElement)( + "div", + { className: "bt-bcards" }, + state.ids.map((tabId, index) => { + const page = pages[tabId] ?? {}; + const label = page.title || shortUrl(page.url) || t("New tab"); +- return (0, import_react31.createElement)( ++ return (0, import_react44.createElement)( + "div", + { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)( + "button", + { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, +- page.excerpt ? (0, import_react31.createElement)( ++ page.excerpt ? (0, import_react44.createElement)( + "span", + { className: "bt-bcard-preview" }, +- (0, import_react31.createElement)("span", { className: "bt-bcard-ptitle" }, label), +- (0, import_react31.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) +- ) : (0, import_react31.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react31.createElement)(GlobeIcon), (0, import_react31.createElement)("span", null, label)) ++ (0, import_react44.createElement)("span", { className: "bt-bcard-ptitle" }, label), ++ (0, import_react44.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) ++ ) : (0, import_react44.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react44.createElement)(GlobeIcon), (0, import_react44.createElement)("span", null, label)) + ), +- (0, import_react31.createElement)("button", { ++ (0, import_react44.createElement)("button", { + type: "button", + className: "bt-bcard-x", + "aria-label": t("Close tab"), +@@ -11367,87 +11367,195 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow + void actions.browserCloseTab(bot.id, tabId); + } + }, "×"), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)( + "div", + { className: "bt-bcard-row" }, +- (0, import_react31.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), +- (0, import_react31.createElement)("span", { className: "bt-bcard-title" }, label), +- page.url ? (0, import_react31.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null ++ (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), ++ (0, import_react44.createElement)("span", { className: "bt-bcard-title" }, label), ++ page.url ? (0, import_react44.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null + ) + ); + }), +- (0, import_react31.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react31.createElement)(PlusIcon), t("New tab")) ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react44.createElement)(PlusIcon), t("New tab")) + ); + } +-function About({ text, title = "About" }) { +- const [open, setOpen] = (0, import_react31.useState)(false); +- const long = text.split("\n").length > 5 || text.length > 240; +- const body = (0, import_react31.useRef)(null); ++function About({ text: text2, title = "About" }) { ++ const [open, setOpen] = (0, import_react44.useState)(false); ++ const long = text2.split("\n").length > 5 || text2.length > 240; ++ const body = (0, import_react44.useRef)(null); + useMorph(body, open); +- return (0, import_react31.createElement)( ++ return (0, import_react44.createElement)( ++ "div", ++ null, ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t(title)), ++ (0, import_react44.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), ++ long ? (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ ); +} -+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])); -+ const list = []; -+ const archived = []; -+ const seen = /* @__PURE__ */ new Set(); -+ for (const row of [...Array.isArray(done) ? done : [], ...Array.isArray(rows) ? rows : []]) { -+ if (!isRow(row)) continue; -+ const id = text(row.id); -+ if (seen.has(id)) continue; -+ seen.add(id); -+ const column = text(row.column) || "not-started"; -+ (column === DONE_COLUMN ? archived : list).push(row); -+ } -+ const projectRow = (row) => { -+ const task = byId.get(text(row.id)); -+ return { -+ id: text(row.id), -+ key: `task:${text(row.id)}`, -+ column: text(row.column) || "not-started", -+ assignee: text(row.assignee) || null, -+ runId: text(row.runId) || null, -+ nodeId: text(row.nodeId) || null, -+ title: text(task?.title) || text(task?.note) || null, -+ priority: text(task?.priority) || null, -+ audit: text(row.audit) || null, -+ // #1831: who is doing the card, and who is speaking for whom. Both are host truth; -+ // a card carrying neither renders exactly as before. -+ holder: text(row.holder) || null, -+ speaksFor: text(row.speaksFor) || null, -+ board: boardOfRow(row, task) ++function SharedRoomApprovals({ room: room2 }) { ++ const [items, setItems] = (0, import_react44.useState)(room2.pendingApprovals ?? []); ++ const [busy, setBusy] = (0, import_react44.useState)(""); ++ const face = room2.origin === "face-to-face"; ++ (0, import_react44.useEffect)(() => { ++ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; ++ if (haveItems) setItems(room2.pendingApprovals); ++ if (room2.viewerRole !== "invited-owner" && !face) return void 0; ++ let alive = true; ++ call("social/status", {}).then((value) => { ++ if (!alive) return; ++ if (room2.viewerRole === "invited-owner" && !haveItems) { ++ setItems(Array.isArray(value?.approvals) ? value.approvals : []); ++ } ++ }, () => { ++ }); ++ return () => { ++ alive = false; + }; ++ }, [room2.id, room2.viewerRole, room2.pendingCount, face]); ++ if (room2.viewerRole !== "invited-owner" && !face || !items.length) return null; ++ const decide = async (item, decision) => { ++ setBusy(item.id); ++ try { ++ await call("social/decide-approval", { approvalId: item.id, decision }); ++ setItems((list) => list.filter((entry) => entry.id !== item.id)); ++ } finally { ++ setBusy(""); ++ } + }; -+ const groups = PROJECT_SOURCES.map((source) => ({ -+ source, -+ label: SOURCE_LABELS[source], -+ rows: list.filter((row) => row.source === source).map(projectRow) -+ })).filter((group) => group.rows.length > 0); -+ const countOf = (value) => value === void 0 || value === null ? null : value; -+ const queueItems = (items, annotate) => (Array.isArray(items) ? items : []).filter((item) => item !== null && typeof item === "object" && text(item.id) !== "").map((item) => { -+ const id = text(item.id); -+ const source = text([...list, ...archived].find((row) => text(row.id) === id)?.source); -+ const task = byId.get(id); -+ return { id, key: `task:${id}`, title: text(task?.title) || text(task?.note) || null, priority: text(task?.priority) || null, board: boardOfRow({ source }, task), ...annotate(item) }; -+ }); -+ const waiting = queueItems(queues?.waiting, (item) => ({ role: text(item.role) || null, waitedMs: Number.isFinite(item.waitedMs) ? item.waitedMs : null })); -+ const inflight = queueItems(queues?.inflight, (item) => ({ holder: text(item.holder) || null, state: item.stalled === true ? "stalled" : "running" })); -+ const blocked = queueItems(queues?.blocked, (item) => ({ reason: text(item.reason) || null })); -+ const queueGroups = [ -+ { group: "waiting", label: "Waiting", items: waiting }, -+ { group: "inflight", label: "In flight", items: inflight }, -+ { group: "blocked", label: "Blocked", items: blocked } -+ ].filter((entry) => entry.items.length > 0); -+ const rawCounts = queues?.counts ?? null; -+ return { -+ projectId: text(project?.id) || null, -+ title: text(project?.name) || "Project", -+ canManage: canManage === true, -+ groups, -+ // #1855: one group, closed until the reader opens it. Rows keep the row shape above, so a -+ // finished card is still a card — only its place on the board changed. -+ done: { group: "done", label: "Done cards", collapsed: true, rows: archived.map(projectRow) }, -+ queues: queueGroups, -+ counts: rawCounts === null ? null : { waiting: countOf(rawCounts.waiting), inflight: countOf(rawCounts.inflight), blocked: countOf(rawCounts.blocked), done: countOf(rawCounts.done) }, -+ count: list.length, -+ empty: list.length === 0 && queueGroups.length === 0 && archived.length === 0, -+ emptyText: "No cards yet.", -+ record: projectBoardRecord({ decisions, meetings }), -+ metrics: metricsView(metrics, { tasks }) -+ }; -+} -+function projectRowNode(row, onOpen) { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); -+ return (0, import_react47.createElement)( ++ return (0, import_react44.createElement)( + "div", -+ { -+ key: row.key, -+ className: "bt-project-row", -+ "data-task-id": row.id, -+ "data-column": row.column, -+ "data-priority": row.priority ?? void 0, -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open, -+ onKeyDown: open === void 0 ? void 0 : (event) => { -+ if (event.key === "Enter" || event.key === " ") open(); ++ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react44.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), ++ items.map((item) => (0, import_react44.createElement)( ++ "div", ++ { key: item.id, className: "bt-set-row" }, ++ (0, import_react44.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) ++ )) ++ ); ++} ++function FaceMembers({ room: room2, roster, actions }) { ++ const people2 = facePeople(room2, roster); ++ const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); ++ return (0, import_react44.createElement)( + "div", + null, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), +- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), +- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ (0, import_react44.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), ++ (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react44.createElement)(FacePersonRow, { ++ key: person.key, ++ person, ++ roster, ++ className: "bt-member", ++ onOpenBot: (id) => actions.openSession(id), ++ lead: room2.face?.lead?.handle ++ }))) + ); + } + function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) { ++ const live = room2.presentedParticipants ?? room2.participants ?? []; ++ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); ++ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); + const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); + const { entries: entries2, now } = useMemberWork(room2, useWork); ++ const invited = room2.viewerRole === "invited-owner"; ++ const title = roomTitle(room2); ++ const leaveMine = () => { ++ const mine = remotes.find((p) => p.ownerId === roster.self?.memberId) || members.find((member) => member.ownerId === roster.self?.memberId); ++ if (mine) void actions.updateRoom(room2.id, { removeParticipants: [{ kind: "bot", id: mine.handle || mine.id, remote: Boolean(mine.handle) }] }); ++ }; ++ const face = room2.origin === "face-to-face"; + return [ +- room2.notice ? (0, import_react31.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react31.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), ++ invited && room2.sharedFrom && !face ? (0, import_react44.createElement)( ++ "div", ++ { key: "from", className: "bt-shared-from" }, ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, title), ++ (0, import_react44.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), ++ room2.sharedFrom.invitedAt ? (0, import_react44.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) ++ ) : face ? null : room2.notice ? (0, import_react44.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react44.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), ++ face ? null : (0, import_react44.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), ++ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react44.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react44.createElement)( + "div", + { key: "members" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })), +- (0, import_react31.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => { +- const entry = entries2.find((item) => item.id === member.id); +- return (0, import_react31.createElement)( ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), ++ (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, [ ++ ...humans.map((person) => (0, import_react44.createElement)( + "div", +- { key: member.id, role: "listitem" }, +- (0, import_react31.createElement)( ++ { key: person.id, role: "listitem" }, ++ (0, import_react44.createElement)( + "div", + { className: "bt-member" }, +- (0, import_react31.createElement)( +- "button", +- { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, +- (0, import_react31.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), +- (0, import_react31.createElement)("span", null, member.name) ++ (0, import_react44.createElement)("span", { className: "bt-member-name" }, (0, import_react44.createElement)("span", null, person.displayName || person.id)), ++ (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Human")), ++ person.role === "admin" ? (0, import_react44.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ++ ) ++ )), ++ ...remotes.map((person) => (0, import_react44.createElement)( ++ "div", ++ { key: person.handle || person.id, role: "listitem" }, ++ (0, import_react44.createElement)( ++ "div", ++ { className: "bt-member" }, ++ (0, import_react44.createElement)( ++ "span", ++ { className: "bt-member-name" }, ++ (0, import_react44.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), ++ (0, import_react44.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) + ), +- room2.admin === member.id ? (0, import_react31.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null +- ), +- entry ? (0, import_react31.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null +- ); +- })) ++ face ? null : (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) ++ ) ++ )), ++ ...members.map((member) => { ++ const entry = entries2.find((item) => item.id === member.id); ++ return (0, import_react44.createElement)( ++ "div", ++ { key: member.id, role: "listitem" }, ++ (0, import_react44.createElement)( ++ "div", ++ { className: "bt-member" }, ++ (0, import_react44.createElement)( ++ "button", ++ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, ++ (0, import_react44.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), ++ (0, import_react44.createElement)("span", null, member.name) ++ ), ++ room2.admin === member.id ? (0, import_react44.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ++ ), ++ entry ? (0, import_react44.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null ++ ); ++ }) ++ ]) + ), +- (0, import_react31.createElement)( ++ face ? null : (0, import_react44.createElement)( + "div", + { key: "mode" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Who answers")), +- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Who answers")), ++ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) + ), +- usage ? (0, import_react31.createElement)( ++ face || !usage ? null : (0, import_react44.createElement)( + "div", + { key: "usage" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), +- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, room2.name), (0, import_react31.createElement)("span", null, usage)) +- ) : null ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Usage")), ++ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, room2.name), (0, import_react44.createElement)("span", null, usage)) ++ ) + ]; + } + function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { + const owner = roster.byId[sessionId]; + const part = owner ? currentPart(owner) : sessionId; + const isMainBot = isMainOf(roster, owner?.id); +- const [tab, setTab] = (0, import_react31.useState)("details"); ++ const [tab, setTab] = (0, import_react44.useState)("details"); + const room2 = roster.roomsById[sessionId]; +- const [view, setView] = (0, import_react31.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); +- const [editing, setEditing] = (0, import_react31.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); +- const [draftName, setDraftName] = (0, import_react31.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); +- const [copied, setCopied] = (0, import_react31.useState)(false); +- const [error, setError] = (0, import_react31.useState)(""); ++ const [view, setView] = (0, import_react44.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); ++ const [editing, setEditing] = (0, import_react44.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); ++ const [draftName, setDraftName] = (0, import_react44.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); ++ const [copied, setCopied] = (0, import_react44.useState)(false); ++ const [error, setError] = (0, import_react44.useState)(""); + const glide = useGlide(tab); +- const last = (0, import_react31.useRef)({ view, tab, move: void 0 }); ++ const last = (0, import_react44.useRef)({ view, tab, move: void 0 }); + if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; + else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; + const move = last.current.move; +@@ -11457,7 +11457,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a + const bot = owner; + const running = useSessionStatus((map) => map.get(part)?.running === true); + const doing = useActivity((map) => map[bot?.id ?? sessionId]); +- const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part])); ++ const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part], copyNames(roster))); + const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); + const run = (promise) => Promise.resolve(promise).then(() => { + setError(""); +@@ -11467,41 +11467,67 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a + return false; + }); + const share = () => { +- void navigator.clipboard?.writeText(transcript).then(() => { ++ const text2 = transcript; ++ if (!text2) { ++ actions.notify?.(t("Nothing to copy yet")); ++ return; ++ } ++ void copiedToClipboard(text2).then((written) => { ++ if (!written) { ++ actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); ++ return; + } -+ }, -+ (0, import_react47.createElement)("span", { className: "bt-project-column" }, t(row.column)), -+ (0, import_react47.createElement)( -+ "span", -+ { className: "bt-project-title-wrap" }, -+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), -+ // 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_react47.createElement)("span", { className: "bt-project-audit" }, row.audit) -+ ), -+ // "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_react47.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), -+ row.speaksFor === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), -+ row.priority === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-priority" }, row.priority), -+ row.runId === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) -+ ); -+} -+function ProjectBoardRows({ view, onOpen }) { -+ if (view.empty) return (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); -+ return view.groups.map((group) => (0, import_react47.createElement)( -+ "section", -+ { key: group.source, className: "bt-project-group", "data-source": group.source }, -+ (0, import_react47.createElement)("h4", null, t(group.label)), -+ group.rows.map((row) => projectRowNode(row, onOpen)) -+ )); -+} -+function ProjectDoneGroup({ view, onOpen }) { -+ if (view.done.rows.length === 0) return null; -+ return (0, import_react47.createElement)( -+ "details", -+ { className: "bt-project-group bt-project-done", "data-group": "done" }, -+ (0, import_react47.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), -+ view.done.rows.map((row) => projectRowNode(row, onOpen)) -+ ); -+} -+function waitedLabel(ms) { -+ if (!Number.isFinite(ms) || ms < 0) return null; -+ const minutes = Math.floor(ms / 6e4); -+ if (minutes < 60) return `${minutes}m`; -+ const hours = Math.floor(minutes / 60); -+ if (hours < 24) return `${hours}h`; -+ return `${Math.floor(hours / 24)}d`; + setCopied(true); + setTimeout(() => setCopied(false), 1600); ++ actions.notify?.(t("Conversation copied as Markdown")); + }); + }; +- const errorLine = error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; +- if ((bot || room2) && view === "settings") { ++ const errorLine = error ? (0, import_react44.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; ++ if ((bot || room2) && view === "settings" && !bot?.remote) { + return [ +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)( + "div", + { key: "head", className: "bt-subhead", "data-move": move }, +- (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react31.createElement)(ChevronLeftIcon)), +- (0, import_react31.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), +- hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) ++ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react44.createElement)(ChevronLeftIcon)), ++ (0, import_react44.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), ++ hasStrip ? null : (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react44.createElement)(CloseIcon)) + ), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)( + "div", + { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, + errorLine, +- bot ? (0, import_react31.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react31.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) ++ bot ? (0, import_react44.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react44.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) + ) + ]; + } +- const top = (0, import_react31.createElement)( ++ const top = (0, import_react44.createElement)( + "div", + { className: "bt-drawer-top", "data-move": side }, +- bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? t("Edit Bot") : t("Group settings"), title: bot ? t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => setView("settings") }, (0, import_react31.createElement)(GearIcon)) : null, +- bot && roster.browser === true && actions.openBrowser ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react31.createElement)(GlobeIcon)) : null, +- bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react31.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react31.createElement)(ShareIcon)) : null, +- hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) ++ bot || room2 ? (0, import_react44.createElement)("button", { ++ type: "button", ++ className: "bt-round", ++ "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), ++ title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), ++ "data-info-row": "settings", ++ onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") ++ }, (0, import_react44.createElement)(GearIcon)) : null, ++ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react44.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react44.createElement)(GlobeIcon)) : null, ++ bot || room2 ? (0, import_react44.createElement)("button", { ++ type: "button", ++ className: "bt-round", ++ // #1347: the accessible name says what the click does, which is copy — "Share conversation" ++ // promised a sharing step that never came. It still says "Copied" after a copy, so a screen ++ // reader hears the same confirmation the toast gives. ++ "aria-label": copied ? t("Copied conversation") : t("Copy conversation"), ++ title: copied ? t("Copied") : t("Copy conversation as Markdown"), ++ onClick: share ++ }, copied ? (0, import_react44.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react44.createElement)(ShareIcon)) : null, ++ hasStrip ? null : (0, import_react44.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react44.createElement)(CloseIcon)) + ); + if (!bot && !room2) { + return [ +- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), +- (0, import_react31.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react31.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) ++ (0, import_react44.createElement)(import_react44.Fragment, { key: "top" }, top), ++ (0, import_react44.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react44.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) + ]; + } + const current = bot?.name ?? room2.name; +@@ -11515,12 +11515,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a + const subtitle = bot ? bot.role : roomSubtitle(room2, roster); + const tabs = bot ? ["details", "browser"] : ["details"]; + return [ +- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)(import_react44.Fragment, { key: "top" }, top), ++ (0, import_react44.createElement)( + "div", + { key: "id", className: "bt-drawer-id", "data-move": side }, +- bot ? (0, import_react31.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), +- editing ? (0, import_react31.createElement)("input", { ++ bot ? (0, import_react44.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react44.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), ++ bot?.remote ? (0, import_react44.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react44.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react44.createElement)("input", { + className: "bt-drawer-name-input", + autoFocus: true, + value: draftName, +@@ -11535,98 +11535,97 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a + setEditing(false); + } + } +- }) : (0, import_react31.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { ++ }) : (0, import_react44.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { + setDraftName(room2 && !room2.named ? "" : current); + setEditing(true); + } }, current), +- subtitle ? (0, import_react31.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null ++ subtitle ? (0, import_react44.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react44.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null + ), +- (0, import_react31.createElement)( ++ bot?.remote ? null : (0, import_react44.createElement)( + "div", + { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, +- (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- tabs.map((key) => (0, import_react31.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) ++ (0, import_react44.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ tabs.map((key) => (0, import_react44.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) + ), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)( + "div", + { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, + errorLine, +- room2 && tab === "details" ? (0, import_react31.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, +- bot && tab === "details" ? [ +- (0, import_react31.createElement)( ++ room2 && tab === "details" ? (0, import_react44.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, ++ bot && bot.remote && tab === "details" ? [ ++ bot.remote.handle ? (0, import_react44.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, ++ (0, import_react44.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) ++ ] : null, ++ bot && !bot.remote && tab === "details" ? [ ++ (0, import_react44.createElement)( + "div", + { key: "model", className: "bt-drawer-model" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Model")), +- (0, import_react31.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), +- modelOf(bot, roster) ? null : (0, import_react31.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Model")), ++ (0, import_react44.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), ++ modelOf(bot, roster) ? null : (0, import_react44.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) + ), +- (0, import_react31.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), +- (0, import_react31.createElement)( ++ (0, import_react44.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), ++ (0, import_react44.createElement)( + "div", + { key: "folder", className: "bt-drawer-model" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Working folder")), +- (0, import_react31.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Working folder")), ++ (0, import_react44.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) + ), +- bot.instructions ? (0, import_react31.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react31.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), +- roster.memory === false ? null : (0, import_react31.createElement)(MemoryRow, { key: "memory", bot, actions }), +- usage ? (0, import_react31.createElement)( ++ bot.instructions ? (0, import_react44.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react44.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), ++ roster.memory === false ? null : (0, import_react44.createElement)(MemoryRow, { key: "memory", bot, actions }), ++ usage ? (0, import_react44.createElement)( + "div", + { key: "usage" }, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), +- (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, bot.name), (0, import_react31.createElement)("span", null, usage)) ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Usage")), ++ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, bot.name), (0, import_react44.createElement)("span", null, usage)) + ) : null, +- (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions }) ++ (0, import_react44.createElement)(ScheduleSummary, { key: "routines", bot, actions }) + ] : null, +- bot && tab === "browser" ? (0, import_react31.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null ++ bot && !bot.remote && tab === "browser" ? (0, import_react44.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null + ) + ]; + } + + // src/client/browser-pane.js +-var import_react32 = require("react"); +-var import_react_dom8 = require("react-dom"); ++var import_react45 = require("react"); ++var import_react_dom10 = require("react-dom"); + var PANEL_LINGER_MS = 300; + var WIDTH_KEY = STORAGE.browserWidth; +-var DEFAULT_WIDTH = 520; +-var DETAILS_WIDTH = 320; +-var DETAILS_MAX = 520; +-var MIN_WIDTH = 360; +-var MIN_CHAT = 420; + var READ_FLASH_MS = 2400; + var EMPTY_PAGE = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, excerpt: "" }; + var EMPTY_TABS2 = { ids: [], active: null }; + var SPRING_LEAD2 = springEasing(576, 39.4); + var SPRING_TRAIL2 = springEasing(144, 19.7); +-var icon = (paths) => function BrowserIcon() { +- return (0, import_react32.createElement)( ++var icon2 = (paths) => function BrowserIcon() { ++ return (0, import_react45.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 }, +- paths.map((d, index) => (0, import_react32.createElement)("path", { key: index, d })) ++ paths.map((d, index) => (0, import_react45.createElement)("path", { key: index, d })) + ); + }; +-var GlobeIcon = icon(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); +-var BackIcon = icon(["M12.5 4.5L7 10l5.5 5.5"]); +-var ForwardIcon = icon(["M7.5 4.5L13 10l-5.5 5.5"]); +-var ReloadIcon = icon(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); +-var StopIcon = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); +-var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); +-var PlusTabIcon = icon(["M10 5v10M5 10h10"]); +-var LockIcon2 = icon(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); ++var GlobeIcon = icon2(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); ++var BackIcon = icon2(["M12.5 4.5L7 10l5.5 5.5"]); ++var ForwardIcon = icon2(["M7.5 4.5L13 10l-5.5 5.5"]); ++var ReloadIcon = icon2(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); ++var StopIcon = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); ++var CloseIcon2 = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); ++var PlusTabIcon = icon2(["M10 5v10M5 10h10"]); ++var LockIcon2 = icon2(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); + function Favicon({ src, className }) { +- const [failed, setFailed] = (0, import_react32.useState)(null); +- if (!src || failed === src) return (0, import_react32.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react32.createElement)(GlobeIcon)); +- return (0, import_react32.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); ++ const [failed, setFailed] = (0, import_react45.useState)(null); ++ if (!src || failed === src) return (0, import_react45.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react45.createElement)(GlobeIcon)); ++ return (0, import_react45.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); + } + var savedWidth = () => { + try { + const value = Number(localStorage.getItem(WIDTH_KEY)); +- return Number.isFinite(value) && value >= DETAILS_WIDTH ? value : null; ++ return Number.isFinite(value) && value >= MIN_DETAILS ? value : null; + } catch { + return null; + } + }; +-var widthRange = (view, viewport) => view === "browser" ? [MIN_WIDTH, viewport - MIN_CHAT] : [DETAILS_WIDTH, Math.min(DETAILS_MAX, viewport - MIN_CHAT)]; + var fitWidth = (width, [low, high]) => Math.round(Math.max(low, Math.min(width, high))); ++var widthRange = (view, viewport) => panelWidthRange(view, viewport); + var room = { rule: null, selector: "" }; + var roomRule = () => { + const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; +@@ -11674,11 +11674,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref + var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); + function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { + const pages = useBrowserPages((map) => map); +- const wrap = (0, import_react32.useRef)(null); +- const ind = (0, import_react32.useRef)(null); +- const spot = (0, import_react32.useRef)(null); +- const under = (0, import_react32.useRef)(null); +- (0, import_react32.useLayoutEffect)(() => { ++ const wrap = (0, import_react45.useRef)(null); ++ const ind = (0, import_react45.useRef)(null); ++ const spot = (0, import_react45.useRef)(null); ++ const under = (0, import_react45.useRef)(null); ++ (0, import_react45.useLayoutEffect)(() => { + const box = wrap.current; + const capsule = ind.current; + if (!box || !capsule) return void 0; +@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, + trail.cancel(); + }; + }); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + const box = wrap.current; + if (!box) return void 0; + const observer = new ResizeObserver(() => { +@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, + observer.observe(box); + return () => observer.disconnect(); + }, []); +- return (0, import_react32.createElement)( ++ return (0, import_react45.createElement)( + "div", + { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, +- (0, import_react32.createElement)( ++ (0, import_react45.createElement)( + "div", + { className: "bt-ptabs-in", ref: wrap }, +- (0, import_react32.createElement)( ++ (0, import_react45.createElement)( + "button", + { + type: "button", +@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, + title: bot.name, + onClick: () => onView("details") + }, +- (0, import_react32.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), +- view === "details" ? (0, import_react32.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null ++ (0, import_react45.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), ++ view === "details" ? (0, import_react45.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null + ), + ids.map((tabId) => { + const page = pages[tabId] ?? EMPTY_PAGE; + const on = view === "browser" && tabId === active; +- return (0, import_react32.createElement)( ++ return (0, import_react45.createElement)( + "button", + { + key: tabId, +@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, + } + } + }, +- (0, import_react32.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), +- (0, import_react32.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), +- (0, import_react32.createElement)("span", { ++ (0, import_react45.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), ++ (0, import_react45.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), ++ (0, import_react45.createElement)("span", { + className: "bt-ptab-x", + role: "button", + "aria-label": t("Close tab"), +@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, + }, "×") + ); + }), +- (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) ++ (0, import_react45.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) + ), + // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. +- (0, import_react32.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react32.createElement)(PlusTabIcon)), +- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react32.createElement)(CloseIcon2)) ++ (0, import_react45.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react45.createElement)(PlusTabIcon)), ++ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react45.createElement)(CloseIcon2)) + ); + } + function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { + const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE; +- const frame = (0, import_react32.useRef)(null); +- const input = (0, import_react32.useRef)(null); +- const [draft, setDraft] = (0, import_react32.useState)(null); ++ const frame = (0, import_react45.useRef)(null); ++ const input = (0, import_react45.useRef)(null); ++ const [draft, setDraft] = (0, import_react45.useState)(null); + const showGuest = landed && !leaving && Boolean(page.url) && !page.error; +- (0, import_react32.useLayoutEffect)(() => { ++ (0, import_react45.useLayoutEffect)(() => { + const element = frame.current; + if (!element || leaving || !tabId) { + void actions.browserShow(null); +@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) + window.removeEventListener("resize", schedule); + }; + }, [tabId, showGuest, leaving]); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + setDraft(null); + if (landed && !page.url) input.current?.focus(); + }, [tabId, landed]); + const go = (event) => { + event.preventDefault(); +- const text = draft ?? ""; +- if (text.trim() === "") return; ++ const text2 = draft ?? ""; ++ if (text2.trim() === "") return; + setDraft(null); + input.current?.blur(); +- void actions.browserNavigate(tabId, text).then((opened) => { ++ void actions.browserNavigate(tabId, text2).then((opened) => { + if (opened) actions.browserFocus(tabId); + }); + }; + const editing = draft !== null; + const secure = /^https:/i.test(page.url); +- return (0, import_react32.createElement)( ++ return (0, import_react45.createElement)( + "div", + { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, +- (0, import_react32.createElement)( ++ (0, import_react45.createElement)( + "div", + { className: "bt-browser-bar" }, +- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react32.createElement)(BackIcon)), +- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react32.createElement)(ForwardIcon)), +- (0, import_react32.createElement)("button", { ++ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react45.createElement)(BackIcon)), ++ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react45.createElement)(ForwardIcon)), ++ (0, import_react45.createElement)("button", { + type: "button", + className: "bt-icon-btn", + disabled: !page.url, + "aria-label": page.loading ? t("Stop") : t("Reload"), + title: page.loading ? t("Stop") : t("Reload"), + onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") +- }, (0, import_react32.createElement)(page.loading ? StopIcon : ReloadIcon)), +- (0, import_react32.createElement)( ++ }, (0, import_react45.createElement)(page.loading ? StopIcon : ReloadIcon)), ++ (0, import_react45.createElement)( + "form", + { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, +- (0, import_react32.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react32.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), +- (0, import_react32.createElement)("input", { ++ (0, import_react45.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react45.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), ++ (0, import_react45.createElement)("input", { + ref: input, + type: "text", + spellCheck: false, +@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) + } + }) + ), +- page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null ++ page.loading ? (0, import_react45.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null + ), +- (0, import_react32.createElement)( ++ (0, import_react45.createElement)( + "div", + { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, +- page.error ? (0, import_react32.createElement)( ++ page.error ? (0, import_react45.createElement)( + "div", + { className: "bt-browser-empty" }, +- (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), +- (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), +- page.url ? (0, import_react32.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { ++ (0, import_react45.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), ++ (0, import_react45.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), ++ page.url ? (0, import_react45.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { + void actions.browserNavigate(tabId, page.url); + } }, t("Try again")) : null +- ) : !page.url ? (0, import_react32.createElement)( ++ ) : !page.url ? (0, import_react45.createElement)( + "div", + { className: "bt-browser-empty" }, +- (0, import_react32.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), +- (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), +- (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) ++ (0, import_react45.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), ++ (0, import_react45.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), ++ (0, import_react45.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) + ) : null + ) + ); +@@ -11907,41 +11907,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu + const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); + const active = tabState.active; + const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); +- const root = (0, import_react32.useRef)(null); +- const fly = (0, import_react32.useRef)(null); +- const [landed, setLanded] = (0, import_react32.useState)(false); +- const [settling, setSettling] = (0, import_react32.useState)(false); +- const [width, setWidth] = (0, import_react32.useState)(savedWidth); +- const [viewport, setViewport] = (0, import_react32.useState)(() => window.innerWidth); +- const [dragging, setDragging] = (0, import_react32.useState)(false); +- const [resizing, setResizing] = (0, import_react32.useState)(false); ++ const root = (0, import_react45.useRef)(null); ++ const fly = (0, import_react45.useRef)(null); ++ const [landed, setLanded] = (0, import_react45.useState)(false); ++ const [settling, setSettling] = (0, import_react45.useState)(false); ++ const [width, setWidth] = (0, import_react45.useState)(savedWidth); ++ const [viewport, setViewport] = (0, import_react45.useState)(() => window.innerWidth); ++ const [dragging, setDragging] = (0, import_react45.useState)(false); ++ const [resizing, setResizing] = (0, import_react45.useState)(false); + const instant = dragging || resizing; +- const [reading, setReading] = (0, import_react32.useState)(false); ++ const [reading, setReading] = (0, import_react45.useState)(false); + const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); +- (0, import_react32.useLayoutEffect)(() => { ++ (0, import_react45.useLayoutEffect)(() => { + if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); + }, [view, width]); +- (0, import_react32.useLayoutEffect)(() => { ++ (0, import_react45.useLayoutEffect)(() => { + chatRoom.hold(instant); + }, [instant]); +- const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth }); ++ const [last, setLast] = (0, import_react45.useState)({ view, width: panelWidth }); + if (last.view !== view || last.width !== panelWidth) { + setLast({ view, width: panelWidth }); + if (last.view !== view) setSettling(last.width !== panelWidth && !instant); + } +- const shownView = (0, import_react32.useRef)(view); +- (0, import_react32.useLayoutEffect)(() => { ++ const shownView = (0, import_react45.useRef)(view); ++ (0, import_react45.useLayoutEffect)(() => { + const switched = settling && shownView.current !== view; + shownView.current = view; + if (switched) chatRoom.hold(true); + chatRoom.set(leaving ? null : panelWidth); + if (switched) chatRoom.hold(false); + }, [view, panelWidth, leaving]); +- (0, import_react32.useEffect)(() => () => { ++ (0, import_react45.useEffect)(() => () => { + chatRoom.set(null); + chatRoom.hold(false); + }, []); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + let timer; + const onResize = () => { + setResizing(true); +@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu + window.removeEventListener("resize", onResize); + }; + }, []); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + if (!settling) return void 0; + const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); + return () => clearTimeout(timer); + }, [settling, view]); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + const timer = setTimeout(() => setLanded(true), 360); + return () => clearTimeout(timer); + }, []); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + if (canBrowse) actions.browserEnsure(bot.id); + }, [bot?.id, canBrowse]); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); + }, [view, active, tabState.ids.length]); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + if (view !== "browser" || leaving) void actions.browserShow?.(null); + }, [view, leaving]); +- (0, import_react32.useEffect)(() => () => { ++ (0, import_react45.useEffect)(() => () => { + void actions.browserShow?.(null); + }, []); +- (0, import_react32.useEffect)(() => { ++ (0, import_react45.useEffect)(() => { + if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; + setReading(true); + const timer = setTimeout(() => setReading(false), READ_FLASH_MS); +@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu + return actions[name](...args); + }; + } +- (0, import_react32.useLayoutEffect)(() => { ++ (0, import_react45.useLayoutEffect)(() => { + const move = fly.current; + if (!move) return; + fly.current = null; +@@ -12096,7 +12096,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu + for (const name of ends) grip.addEventListener(name, onEnd); + }; + const hasStrip = canBrowse && tabState.ids.length > 0; +- return (0, import_react32.createElement)( ++ return (0, import_react45.createElement)( + "aside", + { + ref: root, +@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu + if (event.target === event.currentTarget) setLanded(true); + } + }, +- leaving ? null : (0, import_react32.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), ++ leaving ? null : (0, import_react45.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), + // Over the whole window while dragging: the cursor stays a resize cursor and the + // pages underneath (the chat, the guest) see no pointer. +- dragging ? (0, import_react_dom8.createPortal)((0, import_react32.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, +- hasStrip ? (0, import_react32.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, +- view === "browser" && bot ? (0, import_react32.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react32.createElement)( ++ dragging ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, ++ hasStrip ? (0, import_react45.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, ++ view === "browser" && bot ? (0, import_react45.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react45.createElement)( + "div", + { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, +- (0, import_react32.createElement)(DetailsDrawer, { ++ (0, import_react45.createElement)(DetailsDrawer, { + sessionId: pane.id, + startRenaming: pane.renaming, + startTab: pane.tab, +@@ -12217,9 +12217,79 @@ var BROWSER_CSS = ` + @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} + `; + ++// src/client/remote-bot-settings.js ++var import_react46 = require("react"); ++var REMOTE_BOT_SETTINGS_CSS = ` ++.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} ++.bt-remote-sheet-layer[data-leaving]{pointer-events:none} ++.bt-remote-sheet-layer .bt-scrim{position:absolute;inset:0;z-index:0;pointer-events:auto} ++.bt-remote-sheet-layer[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} ++.bt-remote-sheet:focus{outline:none} ++.bt-remote-sheet{position:relative;z-index:1;pointer-events:auto;width:min(320px,calc(100vw - 32px));max-height:min(70vh,420px);margin:0 auto 0;border-radius:14px;background:var(--bt-card,#fff);border:.5px solid var(--bt-line-2);box-shadow:0 12px 40px rgba(20,20,20,.16);display:flex;flex-direction:column;overflow:hidden;animation:bt-remote-sheet-in .2s ease-out} ++.bt-remote-sheet-layer[data-leaving] .bt-remote-sheet{animation:bt-remote-sheet-out .16s ease-in forwards} ++.bt-remote-sheet-head{display:flex;align-items:center;gap:10px;padding:12px 12px 8px} ++.bt-remote-sheet-who{display:flex;flex-direction:column;min-width:0;gap:1px;flex:1 1 auto} ++.bt-remote-sheet-name{font-size:16px;line-height:22px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-remote-sheet-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-remote-sheet-body{padding:0 12px 14px;overflow:auto} ++@keyframes bt-remote-sheet-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} ++@keyframes bt-remote-sheet-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(8px)}} ++@media (min-width:520px){ ++.bt-remote-sheet-layer{align-items:center;padding:24px} ++.bt-remote-sheet{margin:0} +} -+var METRIC_CARDS = [ -+ { key: "prsOpenedToday", label: "PRs opened today", caption: "Natural day in your time zone, by created_at." }, -+ { key: "prsMergedToday", label: "PRs merged today", caption: "Natural day in your time zone, by merged_at." }, -+ { key: "issuesOpenedToday", label: "Issues opened today", caption: "Natural day in your time zone, by created_at." }, -+ { key: "issuesClosedToday", label: "Issues closed today", caption: "Natural day in your time zone, by closed_at." }, -+ { key: "prMergeMedianMs", label: "PR merge time", caption: "Median of the last 7 days, opened to merged." }, -+ { key: "issueCloseMedianMs", label: "Issue close time", caption: "Median of the last 7 days, opened to closed." }, -+ { key: "taskDoneMedianMs", label: "Task completion", caption: "Median of the last 7 days, created to done." } -+]; -+var METRIC_DASH = "—"; -+var DURATION_KEYS = /* @__PURE__ */ new Set(["prMergeMedianMs", "issueCloseMedianMs", "taskDoneMedianMs"]); -+var RUNNING_OVERDUE_MS = 2 * 60 * 60 * 1e3; -+var metricText = (key, value) => { -+ if (!Number.isFinite(value)) return METRIC_DASH; -+ if (DURATION_KEYS.has(key)) return waitedLabel(value) ?? METRIC_DASH; -+ return String(value); -+}; -+function metricsView(metrics, { tasks } = {}) { -+ const source = metrics !== null && typeof metrics === "object" ? metrics : null; -+ const values = source?.metrics !== null && typeof source?.metrics === "object" ? source.metrics : {}; -+ const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); -+ const running = (Array.isArray(source?.running) ? source.running : []).filter((row) => row !== null && typeof row === "object" && text(row.id) !== "").map((row) => { -+ const id = text(row.id); -+ const task = byId.get(id); -+ return { -+ id, -+ key: "task:" + id, -+ title: text(row.title) || text(task?.title) || text(task?.note) || id, -+ status: text(row.status), -+ who: text(row.who) || null, -+ sinceMs: Number.isFinite(row.sinceMs) ? row.sinceMs : null, -+ source: text(row.source) || null, -+ 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) ++`; ++function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { ++ const close = () => actions.closeSettings(); ++ useEscape(close); ++ const dialog = (0, import_react46.useRef)(null); ++ (0, import_react46.useEffect)(() => { ++ const before = document.activeElement; ++ dialog.current?.focus?.({ preventScroll: true }); ++ return () => { ++ if (before && typeof before.focus === "function" && before.isConnected) before.focus({ preventScroll: true }); + }; -+ }); -+ return { -+ available: source?.ok === true, -+ reason: text(source?.reason) || null, -+ cards: METRIC_CARDS.map((card) => ({ ...card, value: metricText(card.key, values[card.key]) })), -+ running, -+ empty: source === null, -+ emptyText: "No metrics read yet." -+ }; -+} -+function ProjectMetrics({ view }) { -+ return (0, import_react47.createElement)( -+ "section", -+ { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, -+ view.available || view.reason === null ? null : (0, import_react47.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-metric-cards" }, -+ view.cards.map((card) => (0, import_react47.createElement)( -+ "div", -+ { key: card.key, className: "bt-project-metric", "data-metric": card.key }, -+ (0, import_react47.createElement)("span", { className: "bt-project-metric-value" }, card.value), -+ (0, import_react47.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), -+ (0, import_react47.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) -+ )) -+ ) -+ ); -+} -+function ProjectRunningTable({ view, onOpen }) { -+ if (view.running.length === 0) return (0, import_react47.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react47.createElement)("h4", null, t("Running tasks")), (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); -+ return (0, import_react47.createElement)( -+ "section", -+ { className: "bt-project-running", "data-running": String(view.running.length) }, -+ (0, import_react47.createElement)("h4", null, t("Running tasks")), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-running-head" }, -+ (0, import_react47.createElement)("span", null, t("Task")), -+ (0, import_react47.createElement)("span", null, t("Status")), -+ (0, import_react47.createElement)("span", null, t("Who")), -+ (0, import_react47.createElement)("span", null, t("Claimed for")), -+ (0, import_react47.createElement)("span", null, t("Source")), -+ (0, import_react47.createElement)("span", null, t("Last activity")) -+ ), -+ view.running.map((row) => { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); -+ return (0, import_react47.createElement)( -+ "div", -+ { -+ key: row.key, -+ className: "bt-project-running-row", -+ "data-task-id": row.id, -+ "data-overdue": row.overdue ? "true" : "false", -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open -+ }, -+ (0, import_react47.createElement)("span", { className: "bt-project-running-title" }, row.title), -+ (0, import_react47.createElement)("span", { className: "bt-project-running-status" }, t(row.status)), -+ (0, import_react47.createElement)("span", null, row.who ?? METRIC_DASH), -+ (0, import_react47.createElement)("span", null, metricText("since", row.sinceMs)), -+ (0, import_react47.createElement)("span", null, row.source === null ? METRIC_DASH : t(SOURCE_LABELS[row.source] ?? row.source)), -+ (0, import_react47.createElement)("span", null, row.lastActivityAt === null ? METRIC_DASH : new Date(row.lastActivityAt).toLocaleString(dateLocale())) -+ ); -+ }) -+ ); -+} -+function ProjectQueueGroups({ view, onOpen }) { -+ return view.queues.map((entry) => (0, import_react47.createElement)( -+ "section", -+ { key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group }, -+ (0, import_react47.createElement)("h4", null, t(entry.label)), -+ entry.items.map((row) => { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); -+ const note = entry.group === "waiting" ? (0, import_react47.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react47.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react47.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react47.createElement)("span", { className: "bt-project-reason" }, row.reason); -+ return (0, import_react47.createElement)( -+ "div", -+ { -+ key: row.key, -+ className: "bt-project-row", -+ "data-task-id": row.id, -+ "data-state": row.state ?? void 0, -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open, -+ onKeyDown: open === void 0 ? void 0 : (event) => { -+ if (event.key === "Enter" || event.key === " ") open(); -+ } -+ }, -+ (0, import_react47.createElement)( -+ "span", -+ { className: "bt-project-title-wrap" }, -+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`) -+ ), -+ note -+ ); -+ }) -+ )); -+} -+var countText = (value) => value === null ? t("unknown") : String(value); -+function ProjectBoardRecord({ record }) { -+ const { decisions, meetings } = record; -+ const decisionRows = decisions.files.map((file) => (0, import_react47.createElement)( -+ "div", -+ { key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 }, -+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, file.date), -+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), -+ (0, import_react47.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) -+ )); -+ const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); -+ const proxyText = (meeting) => meeting.participants.filter((participant) => participant.speaksFor !== null).map((participant) => `${participant.name} ${t("for {user}", { user: participant.speaksFor })}`).join(", "); -+ const proxyRow = (meeting) => proxyText(meeting) === "" ? null : (0, import_react47.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": meeting.participants.filter((p) => p.speaksFor !== null).map((p) => p.speaksFor).join(", ") }, proxyText(meeting)); -+ const levelOf = (meeting) => (0, import_react47.createElement)("span", { className: "bt-project-level", "data-level": meeting.level }, t(meeting.level)); -+ const decidedRows = meetings.decided.map((meeting) => (0, import_react47.createElement)( -+ "div", -+ { key: meeting.key, className: "bt-project-meeting", "data-status": "decided" }, -+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date), -+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.question), -+ meeting.owner === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`), -+ proxyRow(meeting), -+ // A decided meeting whose file is gone still reads as decided; the missing path says so -+ // instead of the row disappearing with the file. -+ meeting.path === "" ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react47.createElement)("span", { className: "bt-project-path" }, meeting.path) -+ )); -+ const pendingRows = meetings.pending.map((meeting) => (0, import_react47.createElement)( -+ "div", -+ { key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status, "data-level": meeting.level }, -+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date), -+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion), -+ levelOf(meeting), -+ recipientText(meeting.recipients) === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-recipients" }, recipientText(meeting.recipients)), -+ meeting.advance.allowed ? null : (0, import_react47.createElement)("span", { className: "bt-project-paused" }, meeting.advance.reason), -+ proxyRow(meeting), -+ (0, import_react47.createElement)("span", { className: "bt-project-status" }, meeting.status) -+ )); -+ return (0, import_react47.createElement)( -+ "section", -+ { className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) }, -+ (0, import_react47.createElement)("h4", null, t("Decisions")), -+ decisions.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, -+ (0, import_react47.createElement)("h4", null, t("Meetings")), -+ meetings.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows, -+ meetings.pending.length === 0 ? null : (0, import_react47.createElement)("h5", null, t("Not decided")), -+ meetings.pending.length === 0 ? null : pendingRows -+ ); -+} -+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) { -+ const stored = project?.roles !== null && typeof project?.roles === "object" ? project.roles : {}; -+ const owner = []; -+ const addOwner = (id) => { -+ const clean = text(id); -+ if (clean !== "" && !owner.includes(clean)) owner.push(clean); -+ }; -+ addOwner(project?.ownerUserId); -+ for (const id of idList(project?.ownerUserIds)) addOwner(id); -+ const maintainers = idList(stored.maintainers).filter((id) => !owner.includes(id)); -+ const members = idList(stored.members).filter((id) => !owner.includes(id) && !maintainers.includes(id)); -+ return { -+ chief: botIdOf(project?.chief), -+ agent: botIdOf(project?.assistant), -+ roles: { owner, maintainers, members }, -+ empty: owner.length === 0 && maintainers.length === 0 && members.length === 0, -+ // Both come from the `projects` listing (#1828), which the overlay keeps on the record: the -+ // `project-read` record answers no rank, and the panel never re-derives the host's policy. -+ role: text(project?.role), -+ choices: idList(project?.choices) -+ }; -+} -+var teamRow = (role, label, botId, onOpenBot) => (0, import_react47.createElement)( -+ "div", -+ { -+ key: role, -+ className: "bt-project-team-row", -+ "data-team-role": role, -+ // A row with no Bot is the fallback the slice specifies: still drawn, with nothing to open. -+ ...botId === "" ? { "data-team-state": "not-set" } : { -+ "data-open-bot": botId, -+ role: "button", -+ tabIndex: 0, -+ "aria-label": t("Open {name}'s chat", { name: botId }), -+ onClick: () => onOpenBot?.(botId), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" || event.key === " ") { -+ event.preventDefault(); -+ onOpenBot?.(botId); - } -- })(); -- answered.set(id, job); -- if (answered.size > 200) answered.delete(answered.keys().next().value); -+ } -+ } -+ }, -+ (0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), -+ botId === "" ? (0, import_react47.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react47.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`) -+); -+var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]]; -+function ProjectTeam({ project, onOpenBot }) { -+ const view = projectTeamView(project); -+ const ranks = view.empty ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react47.createElement)( -+ "span", -+ { className: "bt-project-team-roles" }, -+ POOL_RANKS.map(([role, key]) => view.roles[key].length === 0 ? null : (0, import_react47.createElement)( -+ "span", -+ { key: role, className: "bt-project-team-role", "data-pool-role": role }, -+ `${roleText(role)}: ${view.roles[key].map((id) => `@${id}`).join(", ")}` -+ )) -+ ); -+ return (0, import_react47.createElement)( -+ "section", -+ { className: "bt-project-team", "data-team": "chief,agent,pool" }, -+ (0, import_react47.createElement)("h4", null, t("Project team")), -+ teamRow("chief", t("Project Chief"), view.chief, onOpenBot), -+ teamRow("agent", t("My agent"), view.agent, onOpenBot), -+ (0, import_react47.createElement)( -+ "div", -+ { key: "pool", className: "bt-project-team-row", "data-team-role": "pool" }, -+ (0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, t("Role pool")), -+ ranks, -+ // The caller's own rank and what the host said they may switch to: the same answer the -+ // sidebar menu reads, printed where the pool is. -+ view.role === "" ? null : (0, import_react47.createElement)( -+ "span", -+ { key: "mine", className: "bt-project-team-myrole", "data-team-my-role": view.role }, -+ [ -+ t("My role"), -+ ": ", -+ roleText(view.role), -+ view.choices.length === 0 ? null : ` · ${t("Can switch to {roles}", { roles: view.choices.map(roleText).join(", ") })}` -+ ] ++ }, []); ++ const onKeyDown = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ close(); ++ } ++ }; ++ if (!bot?.remote) return null; ++ const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); ++ return (0, import_react46.createElement)( ++ "div", ++ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, ++ (0, import_react46.createElement)("div", { className: "bt-scrim", onMouseDown: close }), ++ (0, import_react46.createElement)( ++ "div", ++ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, ++ (0, import_react46.createElement)( ++ "div", ++ { className: "bt-remote-sheet-head" }, ++ (0, import_react46.createElement)(BotMark, { bot, size: 40, live: true }), ++ (0, import_react46.createElement)( ++ "div", ++ { className: "bt-remote-sheet-who" }, ++ (0, import_react46.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), ++ (0, import_react46.createElement)("span", { className: "bt-remote-sheet-sub" }, who) ++ ), ++ (0, import_react46.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react46.createElement)(CloseIcon)) ++ ), ++ (0, import_react46.createElement)( ++ "div", ++ { className: "bt-remote-sheet-body" }, ++ bot.remote.handle ? (0, import_react46.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, ++ (0, import_react46.createElement)(FlyBotDetails, { bot, roster, actions }) + ) + ) + ); +} -+function ProjectBoardPane({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers, onOpenBot }) { -+ const view = projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics }); -+ return (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, -+ (0, import_react47.createElement)("h3", null, view.title), -+ (0, import_react47.createElement)(ProjectTeam, { project, onOpenBot }), -+ // Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown. -+ view.counts === null ? null : (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" }, -+ (0, import_react47.createElement)("span", null, [t("Waiting"), ": ", (0, import_react47.createElement)("b", { key: "w" }, countText(view.counts.waiting))]), -+ (0, import_react47.createElement)("span", null, [t("In flight"), ": ", (0, import_react47.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), -+ (0, import_react47.createElement)("span", null, [t("Blocked"), ": ", (0, import_react47.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), -+ (0, import_react47.createElement)("span", null, [t("Done"), ": ", (0, import_react47.createElement)("b", { key: "d" }, countText(view.counts.done))]) -+ ), -+ // #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_react47.createElement)( -+ "div", -+ { className: "bt-project-actions" }, -+ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), -+ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) -+ ), -+ (0, import_react47.createElement)(ProjectMetrics, { view: view.metrics }), -+ (0, import_react47.createElement)(ProjectRunningTable, { view: view.metrics, onOpen }), -+ (0, import_react47.createElement)(ProjectBoardRows, { view, onOpen }), -+ (0, import_react47.createElement)(ProjectQueueGroups, { view, onOpen }), -+ (0, import_react47.createElement)(ProjectDoneGroup, { view, onOpen }), -+ (0, import_react47.createElement)(ProjectBoardRecord, { record: view.record }) -+ ); ++ + // src/client/overlays.js +-var import_react33 = require("react"); +-var import_react_dom9 = require("react-dom"); ++var import_react47 = require("react"); ++var import_react_dom11 = require("react-dom"); + var DIALOG_EXIT_MS = 160; + var MENU_EXIT_MS3 = 120; + function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { +@@ -12227,15 +12227,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity + const ui2 = useUi((value) => value); + useRegistry((value) => value); + const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); +- const previous = (0, import_react33.useRef)(current); +- (0, import_react33.useEffect)(() => { ++ const previous = (0, import_react47.useRef)(current); ++ (0, import_react47.useEffect)(() => { + if (previous.current === current) return; + previous.current = current; + if (current) actions.followPanel?.(current); + }, [current]); + const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; +- const [kept, setKept] = (0, import_react33.useState)(open); +- (0, import_react33.useEffect)(() => { ++ const [kept, setKept] = (0, import_react47.useState)(open); ++ (0, import_react47.useEffect)(() => { + if (open) { + setKept(open); + return void 0; +@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity + }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); + const pane = open ?? kept; + const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); ++ const remoteBotId = ui2.remoteBotSettings && roster.byId[ui2.remoteBotSettings]?.remote ? ui2.remoteBotSettings : null; ++ const [remoteBotSettings, remoteLeaving] = useLinger(remoteBotId, DIALOG_EXIT_MS); + const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS); + const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); +- return (0, import_react33.createElement)( +- import_react33.Fragment, ++ const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); ++ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); ++ return (0, import_react47.createElement)( ++ import_react47.Fragment, + null, +- pane ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, +- ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, +- ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, +- ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, +- ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, +- menu ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, +- settings ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, +- memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null ++ pane ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, ++ ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, ++ ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, ++ ui2.exchange ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, ++ ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, ++ menu ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, ++ settings ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, ++ remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, ++ memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, ++ inbox ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, ++ board ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null + ); + } + +@@ -12294,19 +12294,19 @@ function readPage(limits) { + const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); + return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); + }; +- const keep = (text2, end = "") => { +- if (text2 === "") return ""; +- stash.push(text2); ++ const keep = (text3, end = "") => { ++ if (text3 === "") return ""; ++ stash.push(text3); + return ` + + ${stash.length - 1}${end} + + `; + }; +- const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); +- const flow = (text2) => text2.split("\n").map((line) => line.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); +- const block = (text2) => expand(flow(text2)); +- const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); ++ const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); ++ const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); ++ const block = (text3) => expand(flow(text3)); ++ const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); + const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); + const childrenOf = (el) => { + if (el.shadowRoot) return el.shadowRoot.childNodes; +@@ -12340,8 +12340,8 @@ function readPage(limits) { + items.push(item); + return { item, number: items.length, secret }; + }; +- const nameOf = (el, text2, secret) => collapse( +- el.getAttribute("aria-label") || text2 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" ++ const nameOf = (el, text3, secret) => collapse( ++ el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" + ).slice(0, 80); + const fenced = (code, lang) => { + const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); +@@ -12372,7 +12372,7 @@ ${fence}`; + const marker = ordered ? `${number++}. ` : "- "; + if (body2 === "") continue; + const pad2 = " ".repeat(marker.length); +- lines.push(body2.split("\n").map((line, index) => (index === 0 ? marker : line ? pad2 : "") + line).join("\n")); ++ lines.push(body2.split("\n").map((line2, index) => (index === 0 ? marker : line2 ? pad2 : "") + line2).join("\n")); + } + return keep(lines.join("\n")); + }; +@@ -12398,11 +12398,11 @@ ${children(cell, ctx)} + let width = 0; + for (const row of rows.slice(0, limits.tableRows + 1)) { + if (over()) break; +- const line = []; ++ const line2 = []; + let column = 0; + const place = () => { + while (carry[column]?.left > 0) { +- line[column] = carry[column].text; ++ line2[column] = carry[column].text; + carry[column].left -= 1; + column += 1; + } +@@ -12410,18 +12410,18 @@ ${children(cell, ctx)} + for (const cell of row.cells) { + place(); + if (styleOf(cell).display === "none") continue; +- const text2 = cellText(cell, ctx); ++ const text3 = cellText(cell, ctx); + const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); + const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); + for (let offset = 0; offset < span; offset += 1) { +- line[column] = offset === 0 ? text2 : ""; +- if (down > 1) carry[column] = { text: offset === 0 ? text2 : "", left: down - 1 }; ++ line2[column] = offset === 0 ? text3 : ""; ++ if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; + column += 1; + } + } + place(); +- width = Math.max(width, line.length); +- grid.push(line); ++ width = Math.max(width, line2.length); ++ grid.push(line2); + } + width = Math.min(width, limits.tableCols); + if (grid.length === 0 || width === 0) return caption ? ` +@@ -12480,9 +12480,9 @@ ${body2 || "(no text)"} + if (node.nodeType === 3) { + if (!ctx.shown) return ""; + const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); +- const text2 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); +- chars += text2.length; +- return text2; ++ const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); ++ chars += text3.length; ++ return text3; + } + if (node.nodeType !== 1) return ""; + const el = node; +@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} + } + if (tag === "blockquote") { + const quoted = block(body2); +- return quoted ? keep(quoted.split("\n").map((line) => line ? `> ${line}` : ">").join("\n")) : ""; ++ return quoted ? keep(quoted.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n")) : ""; + } + const end = body2.trimEnd().length; + const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; +@@ -12570,12 +12570,12 @@ ${marked} + return marked; + } + const body = document.body; +- let text = ""; ++ let text2 = ""; + if (body) { +- text = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); ++ text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); + } +- if (text.length > limits.chars) { +- text = text.slice(0, limits.chars); ++ if (text2.length > limits.chars) { ++ text2 = text2.slice(0, limits.chars); + stop ||= `the text passed ${limits.chars} characters`; + } + const page = { +@@ -12585,7 +12585,7 @@ ${marked} + description: document.querySelector('meta[name="description"]')?.content || "", + selection: String(getSelection() || "").slice(0, 4e3), + format: "markdown", +- text, ++ text: text2, + items, + stats: { ...stats, ms: Math.round(performance.now() - started) } + }; +@@ -12613,15 +12613,15 @@ var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.in + var BLOCKED = "Cloud metadata addresses do not open here."; + var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); + function browserAddress(input) { +- const text = String(input ?? "").trim(); +- if (text === "") return null; +- if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) return /^https?:\/\//i.test(text) && URL.canParse(text) ? new URL(text).href : null; +- if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text)) return null; +- const host = text.split(/[/?#]/)[0]; ++ const text2 = String(input ?? "").trim(); ++ if (text2 === "") return null; ++ if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; ++ if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; ++ const host = text2.split(/[/?#]/)[0]; + const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); +- const looksLikeHost = !/\s/.test(text) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); +- if (looksLikeHost && URL.canParse(`http://${text}`)) return new URL(`${local ? "http" : "https"}://${text}`).href; +- return `${SEARCH_URL}${encodeURIComponent(text)}`; ++ const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); ++ if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; ++ return `${SEARCH_URL}${encodeURIComponent(text2)}`; + } + function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { + const views = /* @__PURE__ */ new Map(); +@@ -13052,11 +13052,118 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random + }; + } + ++// src/client/mobile.js ++var MOBILE_MAX_WIDTH = 640; ++var MOBILE_CSS = ` ++@media (max-width:${MOBILE_MAX_WIDTH}px){ ++.bt-held-actions{max-width:100%} ++.bt-held-card{padding:10px 12px} ++:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px} ++:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer} ++:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} ++:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default} ++.bt-held-retry{min-height:32px} ++.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))} ++.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)} ++.bt-flybot-pointer{max-width:100%;min-width:0} ++.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none} ++.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} ++.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px} ++.bt-inbox-card{flex-wrap:wrap;row-gap:8px} ++.bt-inbox-copy{flex-basis:calc(100% - 38px)} ++/* In progress and Done only. A Needs you ask is the whole text: inbox.js unsets this clamp on those five statuses (#1177). */ ++.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2} ++.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px} ++.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px} ++} ++`; ++ ++// src/client/panes.js ++var PANE_MIN_WIDTH_CSS = ` ++.bt-panel{min-width:0} ++.bt-panel-view{min-width:0} ++.bt-drawer-body{min-width:0;overflow-wrap:anywhere} ++.bt-drawer-body :is(.bt-muted,.bt-note,.bt-section-title,.bt-drawer-role,.bt-drawer-permission){min-width:0;overflow-wrap:anywhere} ++.bt-panel :is(.bt-member,.bt-members,.bt-bcard-row,.bt-drawer-model,.bt-usage-row){min-width:0} ++.bt-panel :is(.bt-muted,.bt-note){white-space:normal} ++[class*="_centerCol"]{min-width:0} ++html,body,#root{overflow-x:clip} ++[class*="_frame"]>[class*="_centerCol"]{overflow-x:clip} ++[class*="_frame"]:has(> [class*="_centerCol"]){overflow-x:clip} ++`; ++var COMPOSER = "[data-composer-input], [data-composer-card]"; ++function clipScroll(el) { ++ if (el && el.scrollLeft > 0) el.scrollLeft = 0; ++} ++function appFrameOf(doc) { ++ if (typeof doc.querySelector !== "function") return null; ++ const col = doc.querySelector('[class*="_centerCol"]'); ++ const frame = col?.parentElement; ++ if (!frame) return null; ++ const name = frame.className; ++ const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; ++ return text2.includes("_frame") ? frame : null; +} -+function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { -+ const save = request ?? call; -+ const [kind, setKind] = (0, import_react47.useState)("bot"); -+ const [memberId, setMemberId] = (0, import_react47.useState)(""); -+ const [note, setNote] = (0, import_react47.useState)(""); -+ const [assistantOff, setAssistantOff] = (0, import_react47.useState)(assistantMine === void 0 ? project?.assistant?.off === true : assistantMine?.off === true); -+ const [status, setStatus] = (0, import_react47.useState)(""); -+ const id = text(project?.id); -+ const run = async (endpoint, payload, done) => { -+ try { -+ const result = await save(endpoint, payload); -+ setStatus(done); -+ onSaved?.(result); -+ return result; -+ } catch (error) { -+ setStatus(hostText(error?.message ?? String(error))); -+ return null; - } -- await post("browser-result", await job).catch((error) => console.warn("[ds-bot] browser result", error)); - }; -- const loop = async () => { -- let failures = 0; -- while (!stopped) { -- poll = new AbortController(); -- const open = browsers.openPages(); -- reported = JSON.stringify(open); -+ return (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-panel", "data-panel": "project-settings" }, -+ (0, import_react47.createElement)("h3", null, t("Project settings")), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ (0, import_react47.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react47.createElement)( -+ "select", -+ { "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) }, -+ (0, import_react47.createElement)("option", { value: "bot" }, t("Bot")), -+ (0, import_react47.createElement)("option", { value: "group" }, t("Group")) -+ ), -+ (0, import_react47.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }), -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")), -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind")) -+ ) -+ ), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ (0, import_react47.createElement)("label", null, t("Project Assistant")), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react47.createElement)("label", null, (0, import_react47.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")), -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save")) -+ ) -+ ), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ // The host knows two memory kinds; the free text the panel takes is recorded as a decision, -+ // never as a third kind the host would refuse (#1820). -+ (0, import_react47.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")), -+ (0, import_react47.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory")) -+ ) -+ ), -+ (0, import_react47.createElement)("div", { className: "bt-project-status-line", role: "status" }, status), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -+ ) -+ ); ++function levelShell(doc, from) { ++ const page = doc.scrollingElement || doc.documentElement; ++ clipScroll(page); ++ clipScroll(doc.documentElement); ++ clipScroll(doc.body); ++ clipScroll(appFrameOf(doc)); ++ for (let at = from?.parentElement || null; at; at = at.parentElement) clipScroll(at); +} -+function ProjectMembersPanel({ project, rows, onClose } = {}) { -+ const owners = []; -+ 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 = []; -+ for (const row of Array.isArray(rows) ? rows : []) { -+ const name = text(row?.assignee); -+ if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name); -+ } -+ const chief = text(project?.chief?.botId); -+ return (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-panel", "data-panel": "project-members" }, -+ (0, import_react47.createElement)("h3", null, t("Members")), -+ (0, import_react47.createElement)( -+ "ul", -+ { className: "bt-project-members" }, -+ owners.map((owner) => (0, import_react47.createElement)( -+ "li", -+ { key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner }, -+ (0, import_react47.createElement)("span", null, `@${owner}`), -+ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Owner")) -+ )), -+ chief === "" ? null : (0, import_react47.createElement)( -+ "li", -+ { key: `chief:${chief}`, className: "bt-project-member", "data-member-role": "chief", "data-member-id": chief }, -+ (0, import_react47.createElement)("span", null, t("Project Chief")), -+ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Chief of this Project")) -+ ), -+ boardMembers.map((name) => (0, import_react47.createElement)( -+ "li", -+ { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, -+ (0, import_react47.createElement)("span", null, `@${name}`), -+ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) -+ )) -+ ), -+ owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, -+ (0, import_react47.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.")), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -+ ) -+ ); ++function composerOf(node) { ++ let el = node; ++ if (el && el.nodeType !== 1) el = el.parentElement; ++ if (!el || typeof el.closest !== "function") return null; ++ return el.closest(COMPOSER); +} -+var PROJECT_BOARD_POLL_MS = 5e3; -+var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project"); -+function ProjectBoardOverlay({ actions, call: request } = {}) { -+ const read = request ?? call; -+ const [state, setState] = (0, import_react47.useState)(null); -+ const [panel, setPanel] = (0, import_react47.useState)(null); -+ const [hash, setHash] = (0, import_react47.useState)(() => typeof window === "undefined" ? "" : window.location.hash); -+ const close = (0, import_react47.useCallback)(() => { -+ if (typeof window !== "undefined") window.location.hash = ""; -+ setPanel(null); -+ setState(null); -+ }, []); -+ (0, import_react47.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ const onKey = (event) => { -+ if (event.key !== "Escape") return; -+ if (panel !== null) setPanel(null); -+ else close(); -+ }; -+ window.addEventListener("keydown", onKey); -+ return () => window.removeEventListener("keydown", onKey); -+ }, [panel, close]); -+ (0, import_react47.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ const sync = () => setHash(window.location.hash); -+ window.addEventListener("hashchange", sync); -+ sync(); -+ return () => window.removeEventListener("hashchange", sync); -+ }, []); -+ (0, import_react47.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ if (!hash.startsWith("#project")) { -+ setState(null); -+ return void 0; -+ } -+ const wanted = hash.slice("#project".length).replace(/^=/, ""); -+ let gone = false; -+ const loadBoard = async () => { -+ const listing = await read("projects"); -+ const projects = listing?.projects ?? []; -+ const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); -+ if (project === void 0) return { project: null, rows: [], canManage: false, tasks: [] }; -+ const [record, board, taskList] = await Promise.all([ -+ read("project-read", { id: project.id }), -+ read("project-board", { id: project.id, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), -+ read("tasks", {}) -+ ]); -+ const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; -+ 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 { project: withRole, assistantMine: record?.assistantMine ?? null, rows: board?.tasks ?? [], done: board?.done ?? [], canManage: record?.canManage === true, tasks, decisions: board?.decisions, meetings: board?.meetings, queues, metrics: board?.metrics ?? null }; -+ }; -+ const refresh = async (first) => { - try { -- const requests = await post("browser-wait", { clientId, open }, poll.signal); -- failures = 0; -- for (const request of Array.isArray(requests) ? requests : []) void answer(request); -+ const next = await loadBoard(); -+ if (!gone) setState(next); - } catch { -- if (stopped) return; -- if (poll.signal.aborted) continue; -- failures += 1; -- await sleep(Math.min(3e4, retryMs * 2 ** Math.min(failures, 5))); -+ if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); - } -- } -- }; -- const offPages = browsers.pages.subscribe(() => { -- if (JSON.stringify(browsers.openPages()) !== reported) poll?.abort(); -- }); -- void loop(); -- return () => { -- stopped = true; -- offPages(); -- poll?.abort(); -- }; -+ }; -+ void refresh(true); -+ const timer = setInterval(() => { -+ if (!boardPollPaused()) void refresh(false); -+ }, PROJECT_BOARD_POLL_MS); -+ return () => { -+ gone = true; -+ clearInterval(timer); -+ }; -+ }, [hash]); -+ if (state === null) return null; -+ return (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-overlay" }, -+ (0, import_react47.createElement)("div", { className: "bt-scrim", onClick: close }), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") }, -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-mem-head" }, -+ (0, import_react47.createElement)("h2", {}, t("Project board")), -+ (0, import_react47.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close")) -+ ), -+ (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-body" }, -+ state.project === null ? (0, import_react47.createElement)( -+ "div", -+ { className: "bt-project-empty" }, -+ (0, import_react47.createElement)("p", null, t("No Project yet — create one.")), -+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ close(); -+ actions?.openSettings?.("projects"); -+ } }, t("Open Projects in Settings")) -+ ) : panel === "settings" ? (0, import_react47.createElement)(ProjectSettingsPanel, { project: state.project, assistantMine: state.assistantMine, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react47.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react47.createElement)(ProjectBoardPane, { -+ project: state.project, -+ rows: state.rows, -+ done: state.done, -+ canManage: state.canManage, -+ tasks: state.tasks, -+ decisions: state.decisions, -+ meetings: state.meetings, -+ queues: state.queues, -+ metrics: state.metrics, -+ onSettings: () => setPanel("settings"), -+ onMembers: () => setPanel("members"), -+ // #1860: a team row opens that Bot's chat. The board is a modal over the conversation, -+ // so it closes first — otherwise the chat opens behind it. -+ onOpenBot: (id) => { -+ close(); -+ actions?.openSession?.(id); -+ }, -+ // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and -+ // lands on that Bot or group. Nothing here re-decides which board that is. -+ onOpen: (board) => { -+ if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`; -+ actions?.openBoard?.(); -+ setState(null); -+ } -+ }) -+ ) -+ ) -+ ); ++function watchShellLevel(doc) { ++ const later = (from) => { ++ const reset = () => levelShell(doc, from); ++ reset(); ++ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); ++ }; ++ const onFocus = (event) => later(composerOf(event.target) || void 0); ++ const onInput = (event) => { ++ const composer = composerOf(event.target); ++ if (composer) later(composer); ++ }; ++ const onScroll = (event) => { ++ const target = event.target; ++ if (target === doc || target === doc.documentElement || target === doc.body) levelShell(doc); ++ }; ++ doc.addEventListener("focusin", onFocus); ++ doc.addEventListener("input", onInput); ++ doc.addEventListener("scroll", onScroll, true); ++ return () => { ++ doc.removeEventListener("focusin", onFocus); ++ doc.removeEventListener("input", onInput); ++ doc.removeEventListener("scroll", onScroll, true); ++ }; +} + +// src/client/friend-lines.js @@ -22975,8 +23422,8 @@ diff --git a/client.js b/client.js + if (known && (shown === void 0 || at > shown) && !isOpen(bot)) unread.push(bot.id); + } + return { known: next, unread }; - } - ++} ++ // src/client/surface.js function groupsFor({ mode, plain = false }) { return { shell: mode === "bot", conversation: mode === "bot" && !plain }; diff --git a/patches/ds-bot/0012-room-membership.patch b/patches/ds-bot/0012-room-membership.patch index 72fb42748..490cd1512 100644 --- a/patches/ds-bot/0012-room-membership.patch +++ b/patches/ds-bot/0012-room-membership.patch @@ -642,7 +642,7 @@ diff --git a/src/client/group-settings.js b/src/client/group-settings.js diff --git a/src/client/header.js b/src/client/header.js --- a/src/client/header.js +++ b/src/client/header.js -@@ -1,11 +1,13 @@ +@@ -1,11 +1,15 @@ import { createElement as h, Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import { currentPart, latestMemoryChange } from './text.js' @@ -654,6 +654,8 @@ diff --git a/src/client/header.js b/src/client/header.js -import { clockLabel, openingTurn, unseenSince } from './cells.js' +import { BusyAckBubble } from './busy-ack.js' import { t } from './i18n.js' ++import { call } from './api.js' ++import { ProjectAgentEntry, projectAgentFor } from './project-board.js' +import { friendBotTitle, roomTitle } from '../shared/remote-label.js' // ------------------------------------------------------------------------- @@ -679,8 +681,13 @@ diff --git a/src/client/header.js b/src/client/header.js const opening = useTurnClock((state) => { const first = openingTurn(state[sessionId], firstTurns, hiddenTurns) return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : '' -@@ -68,41 +68,34 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus +@@ -66,43 +66,42 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus + const owner = roster.byId[sessionId]?.id ?? sessionId + const doing = useActivity(map => map[owner]) const bot = roster.byId[sessionId] ++ // #1864: an owner's Bot that belongs to a Project gets the entry to that Project's assistant in ++ // its own chat header. The roster already carries the binding, so nothing else is read here. ++ const projectAgent = projectAgentFor(bot, roster.self?.memberId) const room = roster.roomsById[sessionId] const detailsOpen = useUi(state => state.details === sessionId) - const name = bot?.name ?? room?.name ?? '' @@ -727,6 +734,9 @@ diff --git a/src/client/header.js b/src/client/header.js h('span', { className: 'bt-sr', role: 'status' }, memoryNotice ? t('Memory updated') : ''), - h(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), - h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })) ++ projectAgent === null ? null : h(ProjectAgentEntry, { ++ projectId: projectAgent.projectId, read: call, onOpen: id => actions?.openSession?.(id), ++ }), + h(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), + h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), + h(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })) diff --git a/patches/ds-bot/0018-project-dossier.patch b/patches/ds-bot/0018-project-dossier.patch deleted file mode 100644 index 1956b8069..000000000 --- a/patches/ds-bot/0018-project-dossier.patch +++ /dev/null @@ -1,21009 +0,0 @@ -FlyBot patch 0018-project-dossier on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/). - -diff --git a/client.js b/client.js ---- a/client.js -+++ b/client.js -@@ -1450,6 +1450,15 @@ var ZH = { - "Role pool": "角色池", - "Can switch to {roles}": "可切换为 {roles}", - "No one in this pool yet.": "角色池里还没有人。", -+ // #1864: the Project team's dossier — persona, memory and skills are read-only in this slice. -+ "Dossier": "档案", -+ "Persona": "人设", -+ "Skills": "技能", -+ "No memory yet.": "还没有记忆。", -+ "No skills attached.": "还没有附加技能。", -+ "Open chat": "打开对话", -+ "Open my agent": "打开我的项目助手", -+ "Open {name}'s dossier": "打开 {name} 的档案", - "No cards yet.": "还没有卡片。", - "Open Projects in Settings": "在设置中打开项目" - }; -@@ -10378,10620 +10378,10789 @@ var BUSY_ACK_CSS = ` - @media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} - `; - --// src/client/header.js -+// src/client/project-board.js - var import_react19 = require("react"); --var import_react_dom6 = require("react-dom"); --var GoArrow = () => (0, import_react19.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_react19.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_react19.useState)(false); -- (0, import_react19.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; --} --function MemoryNotice({ shown }) { -- const inner = (0, import_react19.useRef)(null); -- const [width, setWidth] = (0, import_react19.useState)(0); -- const label = t("Memory updated"); -- (0, import_react19.useLayoutEffect)(() => { -- if (shown) setWidth(inner.current?.scrollWidth ?? 0); -- }, [label, shown]); -- return (0, import_react19.createElement)( -- "span", -- { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, -- (0, import_react19.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react19.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react19.createElement)(MemoryIcon)), label) -- ); -+var PROJECT_BOARD_CSS = ` -+/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the -+ bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the -+ Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */ -+.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} -+.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))} -+.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto} -+.bt-project-board{padding:16px 18px} -+.bt-project-group{margin:0 0 14px} -+.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} -+.bt-project-row[data-column="done"]{opacity:.7} -+.bt-project-title-wrap{display:flex;flex-direction:column;gap:2px;min-width:0} -+.bt-project-audit{font-size:11px;line-height:1.45;white-space:pre-wrap;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} -+.bt-project-empty{padding:22px 4px;opacity:.7} -+/* #1767: the record beside the cards. The same surface as a row, one step quieter, and no new -+ component language: a heading, a list, and a monospace hash the reader can compare. */ -+.bt-project-record{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} -+.bt-project-record h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-record h5{margin:10px 0 4px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;opacity:.55} -+.bt-project-decision,.bt-project-meeting{display:flex;gap:10px;align-items:baseline;padding:6px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} -+.bt-project-decision .bt-project-date,.bt-project-meeting .bt-project-date{font-size:11px;opacity:.6;min-width:76px;font-variant-numeric:tabular-nums} -+.bt-project-decision .bt-project-hash{margin-left:auto;font-size:11px;opacity:.55;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-decision .bt-project-title,.bt-project-meeting .bt-project-title{min-width:0} -+.bt-project-path{font-size:11px;opacity:.6;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-missing{font-size:11px;opacity:.6} -+.bt-project-status{font-size:11px;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-left:auto} -+/* #1831: the card's level, the people it is addressed to, and the identity a row or a comment -+ carries. A severe card is the only one painted louder, because it is the only one that waits. */ -+.bt-project-level{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));opacity:.75} -+.bt-project-level[data-level="severe"]{opacity:1;border-color:#c2410c;color:#c2410c} -+.bt-project-recipients{font-size:11px;opacity:.65} -+.bt-project-paused{font-size:11px;opacity:.75;color:#c2410c} -+.bt-project-holder{font-size:11px;opacity:.7} -+.bt-project-speaks-for{font-size:11px;opacity:.7;font-style:italic} -+.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} -+.bt-project-metrics{margin:0 0 14px} -+.bt-project-metric-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:8px} -+.bt-project-metric{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border:1px solid rgba(127,127,127,.24);border-radius:8px} -+.bt-project-metric-value{font-size:18px;font-weight:600} -+.bt-project-metric-label{font-size:12px;font-weight:600} -+.bt-project-metric-caption{font-size:10px;opacity:.6;line-height:1.3} -+.bt-project-metrics-note{margin:0 0 6px;font-size:11px;opacity:.7} -+.bt-project-running{margin:0 0 14px} -+.bt-project-running-head,.bt-project-running-row{display:grid;grid-template-columns:2fr 1fr 1fr .8fr .8fr 1.2fr;gap:8px;font-size:12px;padding:3px 0} -+.bt-project-running-head{opacity:.6;font-size:10px;letter-spacing:.03em} -+.bt-project-running-row[role="button"]{cursor:pointer} -+.bt-project-running-row[data-overdue="true"]{color:#c2410c} -+.bt-project-counts{display:flex;gap:14px;align-items:center;margin:0 0 12px;font-size:12px;letter-spacing:.03em;opacity:.8} -+.bt-project-counts b{font-weight:600} -+.bt-project-group[data-queue="waiting"] .bt-project-row{cursor:default} -+.bt-project-role{font-size:11px;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-wait{font-size:11px;opacity:.65;margin-left:auto} -+.bt-project-state{font-size:11px;margin-left:auto} -+.bt-project-row[data-state="stalled"]{border-left:2px solid #d9822b} -+.bt-project-reason{font-size:11px;opacity:.7} -+ -+/* #1855: the finished cards are one group of their own, closed until the reader opens it — a -+ Project with months of history reads as the work in flight, not as its archive. */ -+.bt-project-done{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} -+.bt-project-done>summary{cursor:pointer;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7;list-style:none} -+.bt-project-done>summary::-webkit-details-marker{display:none} -+.bt-project-done>summary::before{content:'\\25B8';display:inline-block;width:12px;opacity:.6} -+.bt-project-done[open]>summary::before{content:'\\25BE'} -+.bt-project-done[open]>summary{margin:0 0 8px} -+ -+/* #1749: management panels. Same surface as the board, one form row per control, and a status -+ line that quotes the host's own answer — a refused save reads where the reader pressed it. */ -+.bt-project-panel{padding:16px 18px;display:flex;flex-direction:column;gap:12px} -+.bt-project-panel h3{margin:0} -+.bt-project-field{display:flex;flex-direction:column;gap:4px} -+.bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} -+.bt-project-field textarea{min-height:88px;resize:vertical} -+.bt-project-row-actions{display:flex;gap:8px;align-items:center} -+.bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} -+.bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} -+.bt-project-member .bt-project-member-kind{margin-left:auto;font-size:11px;opacity:.6} -+.bt-project-status-line{font-size:12px;opacity:.75;min-height:16px} -+.bt-project-note{font-size:12px;opacity:.65;margin:0} -+ -+/* #1860: the Project team — who runs the Project, who speaks for the reader, and the role pool. -+ One quiet block above the cards, and a Bot row is a button because a reader is looking a -+ person up. The pool row keeps the rank it plays and the ranks the host said it may switch to. */ -+.bt-project-team{margin:0 0 14px} -+.bt-project-team h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-team-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} -+.bt-project-team-row[data-open-bot]{cursor:pointer} -+.bt-project-team-row[data-open-bot]:hover{background:var(--bt-hover,rgba(127,127,127,.14))} -+.bt-project-team-label{font-size:12px;opacity:.7;min-width:96px} -+.bt-project-team-bot{font-size:12px;font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-team-roles{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.75} -+.bt-project-team-role{font-family:var(--bt-mono,ui-monospace,monospace)} -+.bt-project-team-myrole{margin-left:auto;font-size:11px;opacity:.65} -+ -+/* #1864: the team dossier. A Dossier control on each team row opens one read-only panel; the -+ owner's own chat header carries the same entry to the Project assistant's chat. */ -+.bt-project-team-dossier{margin-left:auto;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:transparent;color:inherit;cursor:pointer} -+.bt-project-team-dossier:hover{background:var(--bt-hover,rgba(127,127,127,.14))} -+.bt-project-dossier{padding:16px 18px;display:flex;flex-direction:column;gap:12px} -+.bt-project-dossier-section{display:flex;flex-direction:column;gap:4px} -+.bt-project-dossier-section h4{margin:0;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} -+.bt-project-dossier-text{margin:0;font-size:13px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere} -+.bt-project-dossier-meta{display:flex;gap:12px;flex-wrap:wrap;font-size:11px;opacity:.7} -+.bt-project-dossier-topics{display:flex;gap:6px;flex-wrap:wrap} -+.bt-project-dossier-topic{font-size:11px;padding:1px 6px;border-radius:999px;background:var(--bt-card,rgba(127,127,127,.08))} -+.bt-project-agent-entry{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:transparent;color:inherit;cursor:pointer} -+.bt-project-agent-entry:hover{background:var(--bt-hover,rgba(127,127,127,.14))} -+`; -+var PROJECT_SOURCES = ["bot", "group", "workflow"]; -+var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; -+var text = (value) => typeof value === "string" ? value.trim() : ""; -+var DONE_COLUMN = "done"; -+var boardOfRow = (row, task) => { -+ if (task === void 0) return null; -+ if (row.source === "group") { -+ const id = text(task.origin?.roomId); -+ return id === "" ? null : { kind: "group", id }; -+ } -+ if (row.source === "bot") { -+ const id = text(task.ownerBotId); -+ return id === "" ? null : { kind: "bot", id }; -+ } -+ return null; -+}; -+var decisionOf = (file) => { -+ if (file === null || typeof file !== "object") return null; -+ const path = text(file.path); -+ if (path === "") return null; -+ return { -+ key: `decision:${path}`, -+ path, -+ name: text(file.name) || path.slice(path.lastIndexOf("/") + 1), -+ date: text(file.date), -+ title: text(file.title), -+ sha256: text(file.sha256), -+ shortHash: text(file.sha256).slice(0, 12) -+ }; -+}; -+var meetingOf = (meeting, status) => { -+ if (meeting === null || typeof meeting !== "object") return null; -+ const id = text(meeting.id); -+ if (id === "") return null; -+ return { -+ key: `meeting:${id}`, -+ id, -+ date: text(meeting.date), -+ question: text(meeting.question) || text(meeting.title), -+ owner: text(meeting.owner) || null, -+ status, -+ path: text(meeting.path), -+ blockingQuestion: text(meeting.blockingQuestion) || null, -+ // #1831: the level and recipients the host answered, and the refusal to advance a severe card. -+ // Nothing is inferred here: a record written before the slice carries an ordinary card and no -+ // recipients, exactly as the host reads it. -+ level: text(meeting.level) || "ordinary", -+ recipients: Array.isArray(meeting.recipients) ? meeting.recipients.map((item) => text(item)).filter((name) => name !== "") : [], -+ advance: meeting.advance !== null && typeof meeting.advance === "object" ? { allowed: meeting.advance.allowed === true, reason: text(meeting.advance.reason) || null } : { allowed: true, reason: null }, -+ // #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 !== "") : [] -+ }; -+}; -+var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; -+function projectBoardRecord({ decisions, meetings } = {}) { -+ const dir = text(decisions?.dir) || null; -+ const files = listOf(decisions?.files); -+ const decided = Array.isArray(meetings?.decided) ? meetings.decided.map((item) => meetingOf(item, "decided")).filter((item) => item !== null) : []; -+ const pending = Array.isArray(meetings?.pending) ? meetings.pending.map((item) => meetingOf(item, item?.status ?? "deliberating")).filter((item) => item !== null) : []; -+ return { -+ decisions: { -+ dir, -+ files, -+ count: files.length, -+ empty: files.length === 0, -+ emptyText: text(decisions?.reason) || (dir === null ? "This Project names no working folder." : `No decision file in ${dir}/decisions.`) -+ }, -+ meetings: { -+ decided, -+ pending, -+ count: decided.length + pending.length, -+ empty: decided.length === 0 && pending.length === 0, -+ emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." -+ } -+ }; - } --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) : ""; -+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])); -+ const list = []; -+ const archived = []; -+ const seen = /* @__PURE__ */ new Set(); -+ for (const row of [...Array.isArray(done) ? done : [], ...Array.isArray(rows) ? rows : []]) { -+ if (!isRow(row)) continue; -+ const id = text(row.id); -+ if (seen.has(id)) continue; -+ seen.add(id); -+ const column = text(row.column) || "not-started"; -+ (column === DONE_COLUMN ? archived : list).push(row); -+ } -+ const projectRow = (row) => { -+ const task = byId.get(text(row.id)); -+ return { -+ id: text(row.id), -+ key: `task:${text(row.id)}`, -+ column: text(row.column) || "not-started", -+ assignee: text(row.assignee) || null, -+ runId: text(row.runId) || null, -+ nodeId: text(row.nodeId) || null, -+ title: text(task?.title) || text(task?.note) || null, -+ priority: text(task?.priority) || null, -+ audit: text(row.audit) || null, -+ // #1831: who is doing the card, and who is speaking for whom. Both are host truth; -+ // a card carrying neither renders exactly as before. -+ holder: text(row.holder) || null, -+ speaksFor: text(row.speaksFor) || null, -+ board: boardOfRow(row, task) -+ }; -+ }; -+ const groups = PROJECT_SOURCES.map((source) => ({ -+ source, -+ label: SOURCE_LABELS[source], -+ rows: list.filter((row) => row.source === source).map(projectRow) -+ })).filter((group) => group.rows.length > 0); -+ const countOf = (value) => value === void 0 || value === null ? null : value; -+ const queueItems = (items, annotate) => (Array.isArray(items) ? items : []).filter((item) => item !== null && typeof item === "object" && text(item.id) !== "").map((item) => { -+ const id = text(item.id); -+ const source = text([...list, ...archived].find((row) => text(row.id) === id)?.source); -+ const task = byId.get(id); -+ return { id, key: `task:${id}`, title: text(task?.title) || text(task?.note) || null, priority: text(task?.priority) || null, board: boardOfRow({ source }, task), ...annotate(item) }; - }); -- (0, import_react19.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 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 changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); -- const memoryNotice = notice && changed2 !== null && roster.memory !== false; -+ const waiting = queueItems(queues?.waiting, (item) => ({ role: text(item.role) || null, waitedMs: Number.isFinite(item.waitedMs) ? item.waitedMs : null })); -+ const inflight = queueItems(queues?.inflight, (item) => ({ holder: text(item.holder) || null, state: item.stalled === true ? "stalled" : "running" })); -+ const blocked = queueItems(queues?.blocked, (item) => ({ reason: text(item.reason) || null })); -+ const queueGroups = [ -+ { group: "waiting", label: "Waiting", items: waiting }, -+ { group: "inflight", label: "In flight", items: inflight }, -+ { group: "blocked", label: "Blocked", items: blocked } -+ ].filter((entry) => entry.items.length > 0); -+ const rawCounts = queues?.counts ?? null; -+ return { -+ projectId: text(project?.id) || null, -+ title: text(project?.name) || "Project", -+ canManage: canManage === true, -+ groups, -+ // #1855: one group, closed until the reader opens it. Rows keep the row shape above, so a -+ // finished card is still a card — only its place on the board changed. -+ done: { group: "done", label: "Done cards", collapsed: true, rows: archived.map(projectRow) }, -+ queues: queueGroups, -+ counts: rawCounts === null ? null : { waiting: countOf(rawCounts.waiting), inflight: countOf(rawCounts.inflight), blocked: countOf(rawCounts.blocked), done: countOf(rawCounts.done) }, -+ count: list.length, -+ empty: list.length === 0 && queueGroups.length === 0 && archived.length === 0, -+ emptyText: "No cards yet.", -+ record: projectBoardRecord({ decisions, meetings }), -+ metrics: metricsView(metrics, { tasks }) -+ }; -+} -+function projectRowNode(row, onOpen) { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); - return (0, import_react19.createElement)( - "div", -- { className: "bt-head" }, -+ { -+ key: row.key, -+ className: "bt-project-row", -+ "data-task-id": row.id, -+ "data-column": row.column, -+ "data-priority": row.priority ?? void 0, -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open, -+ onKeyDown: open === void 0 ? void 0 : (event) => { -+ if (event.key === "Enter" || event.key === " ") open(); -+ } -+ }, -+ (0, import_react19.createElement)("span", { className: "bt-project-column" }, t(row.column)), - (0, import_react19.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[changed2]?.name ?? full2 }) : t("View conversation details"), -- onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId) -- }, -- bot ? (0, import_react19.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -- (0, import_react19.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), -- (0, import_react19.createElement)(MemoryNotice, { shown: memoryNotice }), -- (0, import_react19.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react19.createElement)(GoArrow)) -+ "span", -+ { className: "bt-project-title-wrap" }, -+ (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), -+ // 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_react19.createElement)("span", { className: "bt-project-audit" }, row.audit) - ), -- (0, import_react19.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react19.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), -- (0, import_react19.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), -- (0, import_react19.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) -+ // "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_react19.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), -+ row.speaksFor === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), -+ row.priority === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-priority" }, row.priority), -+ row.runId === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) - ); - } --function useTranscriptView() { -- const [view, setView] = (0, import_react19.useState)(null); -- (0, import_react19.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 () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- document.removeEventListener("scroll", schedule, true); -- window.removeEventListener("resize", schedule); -- }; -- }, []); -- return view; -+function ProjectBoardRows({ view, onOpen }) { -+ if (view.empty) return (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); -+ return view.groups.map((group) => (0, import_react19.createElement)( -+ "section", -+ { key: group.source, className: "bt-project-group", "data-source": group.source }, -+ (0, import_react19.createElement)("h4", null, t(group.label)), -+ group.rows.map((row) => projectRowNode(row, onOpen)) -+ )); - } --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 count; -- }); -- const view = useTranscriptView(); -- const unpinned = view?.unpinned === true; -- const [release, setRelease] = (0, import_react19.useState)({ base: null, dismissed: false }); -- const [above, setAbove] = (0, import_react19.useState)({ seen: false, dismissed: false }); -- (0, import_react19.useEffect)(() => { -- setRelease({ base: unpinned ? replies : null, dismissed: false }); -- }, [unpinned]); -- (0, import_react19.useEffect)(() => { -- if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); -- }, [view?.divider]); -- const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); -- (0, import_react19.useEffect)(() => { -- if (above.seen && movedOn) actions?.settleVisit?.(sessionId); -- }, [above.seen, movedOn, sessionId]); -- const fresh = release.base === null ? 0 : replies - release.base; -- const showDown = unpinned && fresh > 0 && !release.dismissed; -- const showUp = view?.divider === "above" && !above.seen && !above.dismissed; -- (0, import_react19.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_react19.createElement)( -- "div", -- { className: "bt-news", "data-direction": direction, style }, -- (0, import_react19.createElement)( -- "button", -- { type: "button", className: "bt-news-jump", onClick: onJump }, -- (0, import_react19.createElement)("span", { className: "bt-news-ico" }, (0, import_react19.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -- label(count) -- ), -- (0, import_react19.createElement)("button", { -- type: "button", -- className: "bt-news-x", -- "aria-label": t("Dismiss new messages"), -- onClick: (event) => { -- event.stopPropagation(); -- onDismiss(); -- } -- }, (0, import_react19.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 toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); -- return (0, import_react_dom6.createPortal)( -- (0, import_react19.createElement)( -- import_react19.Fragment, -- null, -- showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, -- showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -- ), -- document.body -+function ProjectDoneGroup({ view, onOpen }) { -+ if (view.done.rows.length === 0) return null; -+ return (0, import_react19.createElement)( -+ "details", -+ { className: "bt-project-group bt-project-done", "data-group": "done" }, -+ (0, import_react19.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), -+ view.done.rows.map((row) => projectRowNode(row, onOpen)) - ); - } -- --// src/client/composer-label.js --var ATTRIBUTES = ["aria-label", "data-placeholder"]; --function holdComposerLabel(doc, label) { -- doc.body.style.setProperty("--bt-placeholder", JSON.stringify(label)); -- const shell = /* @__PURE__ */ new Map(); -- const apply2 = () => { -- for (const el of doc.querySelectorAll("[data-composer-input]")) { -- for (const name of ATTRIBUTES) { -- const current = el.getAttribute(name); -- if (current === label) continue; -- if (!shell.has(el)) shell.set(el, {}); -- shell.get(el)[name] = current; -- el.setAttribute(name, label); -- } -- } -- }; -- apply2(); -- const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver; -- const watcher = typeof Observer === "function" ? new Observer(apply2) : null; -- watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES }); -- return () => { -- watcher?.disconnect(); -- for (const [el, values] of shell) { -- if (!el.isConnected) continue; -- for (const [name, value] of Object.entries(values)) { -- if (value == null) el.removeAttribute(name); -- else el.setAttribute(name, value); -- } -- } -- }; -+function waitedLabel(ms) { -+ if (!Number.isFinite(ms) || ms < 0) return null; -+ const minutes = Math.floor(ms / 6e4); -+ if (minutes < 60) return `${minutes}m`; -+ const hours = Math.floor(minutes / 60); -+ if (hours < 24) return `${hours}h`; -+ return `${Math.floor(hours / 24)}d`; - } -- --// src/client/header-effects.js --function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { -- if (!doc) return () => { -- }; -- const slots = /* @__PURE__ */ new Map(); -- const run = (id, dep, setup) => { -- const held = slots.get(id); -- if (held !== void 0 && held.dep === dep) return; -- held?.off(); -- slots.set(id, { dep, off: setup() ?? (() => { -- }) }); -- }; -- let running = false; -- let again = false; -- const pass = () => { -- const list = sessions.getSnapshot(); -- const state = roster.getSnapshot(); -- const row = Object.values(list?.byId ?? {}).find((entry) => (entry?.retainedBy?.mainView ?? 0) > 0); -- const sessionId = row?.id; -- const bot = sessionId === void 0 ? void 0 : state?.byId?.[sessionId]; -- const room2 = sessionId === void 0 ? void 0 : state?.roomsById?.[sessionId]; -- const name = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; -- run("friend-chat", `${sessionId}|${bot?.remote === true}`, () => { -- if (!bot?.remote) return void 0; -- doc.body.dataset.btFriendChat = ""; -- return () => { -- delete doc.body.dataset.btFriendChat; -- }; -- }); -- run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { -- if (!bot?.remote || row?.blank !== true) return void 0; -- doc.body.dataset.btRemoteChat = "empty"; -- doc.body.style.setProperty("--bt-remote-empty", JSON.stringify(t("Send a message. Your friend approves unless Auto-send is on."))); -- return () => { -- delete doc.body.dataset.btRemoteChat; -- doc.body.style.removeProperty("--bt-remote-empty"); -- }; -- }); -- run("composer-label", `${sessionId}|${name}`, () => holdComposerLabel(doc, name ? t("Message {name}", { name }) : t("Message"))); -- run("visit", sessionId, () => { -- if (sessionId === void 0) return void 0; -- actions.beginVisit(sessionId); -- return () => actions.endVisit(sessionId); -- }); -- run("face-at-menu", `${sessionId}|${room2?.origin === "face-to-face"}`, () => { -- if (room2?.origin !== "face-to-face") return void 0; -- const stop = actions.filterFaceAtMenu?.(sessionId); -- return typeof stop === "function" ? stop : void 0; -- }); -- run("room-flag", `${sessionId}|${room2 !== void 0}`, () => { -- if (room2 === void 0) return void 0; -- doc.body.dataset.btRoom = ""; -- return () => { -- delete doc.body.dataset.btRoom; -- }; -- }); -- }; -- const apply2 = () => { -- if (running) { -- again = true; -- return; -- } -- running = true; -- try { -- pass(); -- } finally { -- running = false; -- } -- if (again) { -- again = false; -- apply2(); -- } -- }; -- apply2(); -- const offSessions = sessions.subscribe(apply2); -- const offRoster = roster.subscribe(apply2); -- return () => { -- offSessions?.(); -- offRoster?.(); -- for (const held of slots.values()) held.off(); -- slots.clear(); -- }; --} -- --// src/client/composer.js --var import_react20 = require("react"); --var import_react_dom7 = require("react-dom"); --var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; --var keepComposerFocus = (event) => event.preventDefault(); --function useDictation(onText) { -- const [listening, setListening] = (0, import_react20.useState)(false); -- const [note, setNote] = (0, import_react20.useState)(""); -- const recognition = (0, import_react20.useRef)(null); -- const latest = (0, import_react20.useRef)(onText); -- latest.current = onText; -- (0, import_react20.useEffect)(() => () => recognition.current?.abort(), []); -- (0, import_react20.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]; -- const text2 = result[0]?.transcript ?? ""; -- if (result.isFinal) { -- 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?.(); -+var METRIC_CARDS = [ -+ { key: "prsOpenedToday", label: "PRs opened today", caption: "Natural day in your time zone, by created_at." }, -+ { key: "prsMergedToday", label: "PRs merged today", caption: "Natural day in your time zone, by merged_at." }, -+ { key: "issuesOpenedToday", label: "Issues opened today", caption: "Natural day in your time zone, by created_at." }, -+ { key: "issuesClosedToday", label: "Issues closed today", caption: "Natural day in your time zone, by closed_at." }, -+ { key: "prMergeMedianMs", label: "PR merge time", caption: "Median of the last 7 days, opened to merged." }, -+ { key: "issueCloseMedianMs", label: "Issue close time", caption: "Median of the last 7 days, opened to closed." }, -+ { key: "taskDoneMedianMs", label: "Task completion", caption: "Median of the last 7 days, created to done." } -+]; -+var METRIC_DASH = "—"; -+var DURATION_KEYS = /* @__PURE__ */ new Set(["prMergeMedianMs", "issueCloseMedianMs", "taskDoneMedianMs"]); -+var RUNNING_OVERDUE_MS = 2 * 60 * 60 * 1e3; -+var metricText = (key, value) => { -+ if (!Number.isFinite(value)) return METRIC_DASH; -+ if (DURATION_KEYS.has(key)) return waitedLabel(value) ?? METRIC_DASH; -+ return String(value); -+}; -+function metricsView(metrics, { tasks } = {}) { -+ const source = metrics !== null && typeof metrics === "object" ? metrics : null; -+ const values = source?.metrics !== null && typeof source?.metrics === "object" ? source.metrics : {}; -+ const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); -+ const running = (Array.isArray(source?.running) ? source.running : []).filter((row) => row !== null && typeof row === "object" && text(row.id) !== "").map((row) => { -+ const id = text(row.id); -+ const task = byId.get(id); -+ return { -+ id, -+ key: "task:" + id, -+ title: text(row.title) || text(task?.title) || text(task?.note) || id, -+ status: text(row.status), -+ who: text(row.who) || null, -+ sinceMs: Number.isFinite(row.sinceMs) ? row.sinceMs : null, -+ source: text(row.source) || null, -+ 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) - }; -- recognition.current = engine; -- try { -- engine.start(); -- setListening(true); -- return true; -- } catch { -- setNote(t("Couldn't start dictation")); -- return false; -- } -+ }); -+ return { -+ available: source?.ok === true, -+ reason: text(source?.reason) || null, -+ cards: METRIC_CARDS.map((card) => ({ ...card, value: metricText(card.key, values[card.key]) })), -+ running, -+ empty: source === null, -+ emptyText: "No metrics read yet." - }; -- const stop = () => recognition.current?.stop(); -- return { listening, note, start, stop }; - } --function ComposerIdentity({ sessionId, useRoster }) { -- const roster = useRoster((value) => value); -- const room2 = roster.roomsById?.[sessionId]; -- const self = roster.self; -- const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); -- if (room2?.viewerRole === "invited-owner") { -- return (0, import_react20.createElement)( -- "span", -- { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, -- t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) -- ); -- } -- if (!self || !room2 || humans.length < 2) return null; -- return (0, import_react20.createElement)( -- "span", -- { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, -- t("Sending as {name}", { name: self.displayName || self.memberId }) -+function ProjectMetrics({ view }) { -+ return (0, import_react19.createElement)( -+ "section", -+ { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, -+ view.available || view.reason === null ? null : (0, import_react19.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-metric-cards" }, -+ view.cards.map((card) => (0, import_react19.createElement)( -+ "div", -+ { key: card.key, className: "bt-project-metric", "data-metric": card.key }, -+ (0, import_react19.createElement)("span", { className: "bt-project-metric-value" }, card.value), -+ (0, import_react19.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), -+ (0, import_react19.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) -+ )) -+ ) - ); - } --function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -- const dictation = useDictation((text2, final) => { -- if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); -- }); -- const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -- (0, import_react20.useEffect)(() => { -- if (!quote || !inputActions) return; -- actions.clearQuote(quote.token); -- const excerpt = quote.text.replace(/\s+/g, " ").trim(); -- const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} -- --`; -- inputActions.insertText(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; -- return (0, import_react20.createElement)( -- import_react20.Fragment, -- null, -- pluginMic ? null : (0, import_react20.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_react20.createElement)(MicIcon)), -- (0, import_react20.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_react20.createElement)(WaveIcon, { size: 18 })), -- dictation.note ? (0, import_react20.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null -+function ProjectRunningTable({ view, onOpen }) { -+ if (view.running.length === 0) return (0, import_react19.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react19.createElement)("h4", null, t("Running tasks")), (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); -+ return (0, import_react19.createElement)( -+ "section", -+ { className: "bt-project-running", "data-running": String(view.running.length) }, -+ (0, import_react19.createElement)("h4", null, t("Running tasks")), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-running-head" }, -+ (0, import_react19.createElement)("span", null, t("Task")), -+ (0, import_react19.createElement)("span", null, t("Status")), -+ (0, import_react19.createElement)("span", null, t("Who")), -+ (0, import_react19.createElement)("span", null, t("Claimed for")), -+ (0, import_react19.createElement)("span", null, t("Source")), -+ (0, import_react19.createElement)("span", null, t("Last activity")) -+ ), -+ view.running.map((row) => { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); -+ return (0, import_react19.createElement)( -+ "div", -+ { -+ key: row.key, -+ className: "bt-project-running-row", -+ "data-task-id": row.id, -+ "data-overdue": row.overdue ? "true" : "false", -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open -+ }, -+ (0, import_react19.createElement)("span", { className: "bt-project-running-title" }, row.title), -+ (0, import_react19.createElement)("span", { className: "bt-project-running-status" }, t(row.status)), -+ (0, import_react19.createElement)("span", null, row.who ?? METRIC_DASH), -+ (0, import_react19.createElement)("span", null, metricText("since", row.sinceMs)), -+ (0, import_react19.createElement)("span", null, row.source === null ? METRIC_DASH : t(SOURCE_LABELS[row.source] ?? row.source)), -+ (0, import_react19.createElement)("span", null, row.lastActivityAt === null ? METRIC_DASH : new Date(row.lastActivityAt).toLocaleString(dateLocale())) -+ ); -+ }) - ); - } --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_react20.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); -- return marks.get(id); -- }; -+function ProjectQueueGroups({ view, onOpen }) { -+ return view.queues.map((entry) => (0, import_react19.createElement)( -+ "section", -+ { key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group }, -+ (0, import_react19.createElement)("h4", null, t(entry.label)), -+ entry.items.map((row) => { -+ const open = row.board === null ? void 0 : () => onOpen?.(row.board); -+ const note = entry.group === "waiting" ? (0, import_react19.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react19.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react19.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react19.createElement)("span", { className: "bt-project-reason" }, row.reason); -+ return (0, import_react19.createElement)( -+ "div", -+ { -+ key: row.key, -+ className: "bt-project-row", -+ "data-task-id": row.id, -+ "data-state": row.state ?? void 0, -+ role: open === void 0 ? void 0 : "button", -+ tabIndex: open === void 0 ? void 0 : 0, -+ onClick: open, -+ onKeyDown: open === void 0 ? void 0 : (event) => { -+ if (event.key === "Enter" || event.key === " ") open(); -+ } -+ }, -+ (0, import_react19.createElement)( -+ "span", -+ { className: "bt-project-title-wrap" }, -+ (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`) -+ ), -+ note -+ ); -+ }) -+ )); - } --function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { -- const roster = useRoster((value) => value); -- const [open, setOpen] = (0, import_react20.useState)(null); -- const [shown, leaving] = useLinger(open, 120); -- const button = (0, import_react20.useRef)(null); -- const menu = (0, import_react20.useRef)(null); -- (0, import_react20.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setOpen(null); -- } -- }; -- window.addEventListener("mousedown", onDown, true); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown, true); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [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 }); -- }; -- const attach = () => { -- setOpen(null); -- button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); -- }; -- const schedule = () => { -- setOpen(null); -- actions.openSettings("schedules", target); -+var countText = (value) => value === null ? t("unknown") : String(value); -+function ProjectBoardRecord({ record }) { -+ const { decisions, meetings } = record; -+ const decisionRows = decisions.files.map((file) => (0, import_react19.createElement)( -+ "div", -+ { key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 }, -+ (0, import_react19.createElement)("span", { className: "bt-project-date" }, file.date), -+ (0, import_react19.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), -+ (0, import_react19.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) -+ )); -+ const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); -+ const proxyText = (meeting) => meeting.participants.filter((participant) => participant.speaksFor !== null).map((participant) => `${participant.name} ${t("for {user}", { user: participant.speaksFor })}`).join(", "); -+ const proxyRow = (meeting) => proxyText(meeting) === "" ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": meeting.participants.filter((p) => p.speaksFor !== null).map((p) => p.speaksFor).join(", ") }, proxyText(meeting)); -+ const levelOf = (meeting) => (0, import_react19.createElement)("span", { className: "bt-project-level", "data-level": meeting.level }, t(meeting.level)); -+ const decidedRows = meetings.decided.map((meeting) => (0, import_react19.createElement)( -+ "div", -+ { key: meeting.key, className: "bt-project-meeting", "data-status": "decided" }, -+ (0, import_react19.createElement)("span", { className: "bt-project-date" }, meeting.date), -+ (0, import_react19.createElement)("span", { className: "bt-project-title" }, meeting.question), -+ meeting.owner === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`), -+ proxyRow(meeting), -+ // A decided meeting whose file is gone still reads as decided; the missing path says so -+ // instead of the row disappearing with the file. -+ meeting.path === "" ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react19.createElement)("span", { className: "bt-project-path" }, meeting.path) -+ )); -+ const pendingRows = meetings.pending.map((meeting) => (0, import_react19.createElement)( -+ "div", -+ { key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status, "data-level": meeting.level }, -+ (0, import_react19.createElement)("span", { className: "bt-project-date" }, meeting.date), -+ (0, import_react19.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion), -+ levelOf(meeting), -+ recipientText(meeting.recipients) === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-recipients" }, recipientText(meeting.recipients)), -+ meeting.advance.allowed ? null : (0, import_react19.createElement)("span", { className: "bt-project-paused" }, meeting.advance.reason), -+ proxyRow(meeting), -+ (0, import_react19.createElement)("span", { className: "bt-project-status" }, meeting.status) -+ )); -+ return (0, import_react19.createElement)( -+ "section", -+ { className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) }, -+ (0, import_react19.createElement)("h4", null, t("Decisions")), -+ decisions.empty ? (0, import_react19.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, -+ (0, import_react19.createElement)("h4", null, t("Meetings")), -+ meetings.empty ? (0, import_react19.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows, -+ meetings.pending.length === 0 ? null : (0, import_react19.createElement)("h5", null, t("Not decided")), -+ meetings.pending.length === 0 ? null : pendingRows -+ ); -+} -+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) { -+ const stored = project?.roles !== null && typeof project?.roles === "object" ? project.roles : {}; -+ const owner = []; -+ const addOwner = (id) => { -+ const clean = text(id); -+ if (clean !== "" && !owner.includes(clean)) owner.push(clean); - }; -- const reference = () => { -- setOpen(null); -- actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); -+ addOwner(project?.ownerUserId); -+ for (const id of idList(project?.ownerUserIds)) addOwner(id); -+ const maintainers = idList(stored.maintainers).filter((id) => !owner.includes(id)); -+ const members = idList(stored.members).filter((id) => !owner.includes(id) && !maintainers.includes(id)); -+ return { -+ chief: botIdOf(project?.chief), -+ agent: botIdOf(project?.assistant), -+ roles: { owner, maintainers, members }, -+ empty: owner.length === 0 && maintainers.length === 0 && members.length === 0, -+ // Both come from the `projects` listing (#1828), which the overlay keeps on the record: the -+ // `project-read` record answers no rank, and the panel never re-derives the host's policy. -+ role: text(project?.role), -+ choices: idList(project?.choices) - }; -- const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); -- return (0, import_react20.createElement)( -- import_react20.Fragment, -- null, -- (0, import_react20.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_react20.createElement)(PlusIcon)), -- shown ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)( -+} -+var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react19.createElement)( -+ "div", -+ { -+ key: role, -+ className: "bt-project-team-row", -+ "data-team-role": role, -+ // A row with no Bot is the fallback the slice specifies: still drawn, with nothing to open. -+ ...botId === "" ? { "data-team-state": "not-set" } : { -+ "data-open-bot": botId, -+ role: "button", -+ tabIndex: 0, -+ "aria-label": t("Open {name}'s chat", { name: botId }), -+ onClick: () => onOpenBot?.(botId), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" || event.key === " ") { -+ event.preventDefault(); -+ onOpenBot?.(botId); -+ } -+ } -+ } -+ }, -+ (0, import_react19.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), -+ botId === "" ? (0, import_react19.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react19.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`), -+ // #1864: the dossier control sits inside the row, so the row still opens the chat (#1860) and a -+ // reader can look a Bot up first. A row with no Bot keeps the control: its panel reads "not set". -+ (0, import_react19.createElement)("button", { -+ key: "dossier", -+ type: "button", -+ className: "bt-project-team-dossier", -+ "data-dossier-role": role, -+ ...botId === "" ? {} : { "data-dossier-bot": botId }, -+ "aria-label": t("Open {name}'s dossier", { name: botId === "" ? label : botId }), -+ onClick: (event) => { -+ event?.stopPropagation(); -+ onOpenDossier?.({ role, label, botId }); -+ }, -+ onKeyDown: (event) => { -+ if (event.key === "Enter" || event.key === " ") { -+ event?.stopPropagation(); -+ onOpenDossier?.({ role, label, botId }); -+ } -+ } -+ }, t("Dossier")) -+); -+var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]]; -+function ProjectTeam({ project, onOpenBot, onOpenDossier }) { -+ const view = projectTeamView(project); -+ const poolId = (id) => (0, import_react19.createElement)("button", { -+ key: id, -+ type: "button", -+ className: "bt-project-team-dossier", -+ "data-dossier-role": "pool", -+ "data-dossier-bot": id, -+ "aria-label": t("Open {name}'s dossier", { name: id }), -+ onClick: (event) => { -+ event?.stopPropagation(); -+ onOpenDossier?.({ role: "pool", label: t("Role pool"), botId: id }); -+ } -+ }, `@${id}`); -+ const ranks = view.empty ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react19.createElement)( -+ "span", -+ { className: "bt-project-team-roles" }, -+ POOL_RANKS.map(([role, key]) => view.roles[key].length === 0 ? null : (0, import_react19.createElement)( -+ "span", -+ { key: role, className: "bt-project-team-role", "data-pool-role": role }, -+ `${roleText(role)}: `, -+ view.roles[key].map((id) => poolId(id)) -+ )) -+ ); -+ return (0, import_react19.createElement)( -+ "section", -+ { className: "bt-project-team", "data-team": "chief,agent,pool" }, -+ (0, import_react19.createElement)("h4", null, t("Project team")), -+ teamRow("chief", t("Project Chief"), view.chief, onOpenBot, onOpenDossier), -+ teamRow("agent", t("My agent"), view.agent, onOpenBot, onOpenDossier), -+ (0, import_react19.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_react20.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react20.createElement)(PaperclipIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -- canReference ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react20.createElement)(ChatsIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -- (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react20.createElement)(ClockIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -- ), document.body) : null -+ { key: "pool", className: "bt-project-team-row", "data-team-role": "pool" }, -+ (0, import_react19.createElement)("span", { key: "label", className: "bt-project-team-label" }, t("Role pool")), -+ ranks, -+ // The caller's own rank and what the host said they may switch to: the same answer the -+ // sidebar menu reads, printed where the pool is. -+ view.role === "" ? null : (0, import_react19.createElement)( -+ "span", -+ { key: "mine", className: "bt-project-team-myrole", "data-team-my-role": view.role }, -+ [ -+ t("My role"), -+ ": ", -+ roleText(view.role), -+ view.choices.length === 0 ? null : ` · ${t("Can switch to {roles}", { roles: view.choices.map(roleText).join(", ") })}` -+ ] -+ ) -+ ) - ); - } -- --// src/client/at-menu.js --var BOTS_SOURCE = "ds-bot"; --var CHATS_SOURCE = "ds-bot-chats"; --var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; --var OTHER_CHATS_MAX = 8; --var conversationOf = (roster, sessionId) => { -- if (!roster.ready) return null; -- const room2 = roster.roomsById[sessionId]; -- if (room2) return { room: room2 }; -- const self = roster.byId[sessionId]; -- return self ? { self } : null; --}; --function mentionRows(roster, sessionId, query = "") { -- const here = conversationOf(roster, sessionId); -- if (here === null) return null; -- const face = isFaceRoom(here.room); -- const live = (here.room?.participants ?? []).filter((p) => p.leftSeq === void 0); -- const bots = here.room ? [ -- ...here.room.members.map((id) => roster.byId[id]).filter(Boolean).map((bot) => face ? { ...bot, label: faceLabel(myOwnerName(roster, live, bot), bot.name) } : bot), -- ...live.filter((p) => p.kind === "human").map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role })), -- // A remote seat already listed under its handle (a duplicate participant record) is offered once. -- ...uniqueBy(live.filter((p) => p.remote), (p) => p.handle || p.id).map((p) => ({ -- id: p.handle || p.id, -- // A face room inserts the disambiguated title. An ordinary group inserts the name the -- // room routes on (#1141): never "@@handle.home (owner)" and never "(Friend)". -- name: face ? remoteBotTitle(p) : remoteCallName(p), -- kind: "bot", -- remote: p, -- handle: p.handle, -- label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) -- })) -- ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); -- const needle = query.trim().toLowerCase(); -- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); -- const everyone = t("Everyone"); -- if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -- rows.push({ room: here.room, count: bots.filter((bot) => bot.kind !== "human").length, text: `@${everyone} ` }); -- } -- 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)))]; -- const remoteHandles = new Set(remoteMentionRows(roster, "").map((row) => normHandle(row.remote.handle))); -- return [ -- ...rows.filter((row) => !(row.bot && remoteHandles.has(normHandle(remoteRecord(row.bot)?.handle)))), -- ...remoteMentionRows(roster, query) -- ]; -+function dossierRowOf(team, botId) { -+ const id = text(botId); -+ if (id === "") return null; -+ const rows = Array.isArray(team?.rows) ? team.rows : []; -+ return rows.find((row) => row !== null && typeof row === "object" && text(row.botId) === id) ?? null; - } --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 }); --var normHandle = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); --var remoteRecord = (bot) => bot?.remote ? bot.remote === true ? bot : bot.remote : null; --var myOwnerName = (roster, live, bot) => { -- const seat = live.find((p) => !p.remote && p.kind !== "human" && (p.id === bot.id || bot.handle && p.handle === bot.handle)); -- return seatOwnerName(seat?.ownerName, seat?.handle) || cleanPeerName(roster.self?.displayName, ""); --}; --var uniqueBy = (list, key) => { -- const seen = /* @__PURE__ */ new Set(); -- return list.filter((item) => { -- const k = key(item); -- if (!k || seen.has(k)) return !k; -- seen.add(k); -- return true; -- }); --}; --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})`; -+var dossierSection = (title, children) => (0, import_react19.createElement)( -+ "div", -+ { key: title, className: "bt-project-dossier-section" }, -+ (0, import_react19.createElement)("h4", null, title), -+ children -+); -+function ProjectDossierPanel({ row, label, onClose, onOpenBot } = {}) { -+ const known = row !== null && typeof row === "object"; -+ const botId = known ? text(row.botId) : ""; -+ const topics = known && Array.isArray(row.memory?.topics) ? row.memory.topics.map((name) => text(name)).filter((name) => name !== "") : []; -+ const skills = known && Array.isArray(row.skills) ? row.skills.map((name) => text(name)).filter((name) => name !== "") : []; -+ const meta = known ? [text(row.role), text(row.model)].filter((value) => value !== "") : []; -+ return (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-panel bt-project-dossier", "data-panel": "project-dossier", "data-dossier-bot": botId === "" ? void 0 : botId }, -+ (0, import_react19.createElement)("h3", null, label === "" || label === void 0 ? t("Dossier") : `${label} · ${t("Dossier")}`), -+ known ? [ -+ dossierSection(t("Persona"), [ -+ meta.length === 0 ? null : (0, import_react19.createElement)("div", { key: "meta", className: "bt-project-dossier-meta" }, meta.map((value) => (0, import_react19.createElement)("span", { key: value }, value))), -+ (0, import_react19.createElement)("p", { key: "persona", className: "bt-project-dossier-text" }, text(row.persona) || t("Not set")) -+ ]), -+ dossierSection(t("Memory"), [ -+ (0, import_react19.createElement)("p", { key: "head", className: "bt-project-dossier-text" }, text(row.memory?.head) || t("No memory yet.")), -+ topics.length === 0 ? null : (0, import_react19.createElement)("div", { key: "topics", className: "bt-project-dossier-topics" }, topics.map((topic) => (0, import_react19.createElement)("span", { key: topic, className: "bt-project-dossier-topic" }, topic))) -+ ]), -+ dossierSection(t("Skills"), (0, import_react19.createElement)("p", { key: "skills", className: "bt-project-dossier-text" }, skills.length === 0 ? t("No skills attached.") : skills.join(", "))) -+ ] : (0, import_react19.createElement)("p", { className: "bt-project-missing" }, t("Not set")), -+ botId === "" || !known ? null : (0, import_react19.createElement)( -+ "div", -+ { key: "actions", className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("button", { -+ type: "button", -+ className: "bt-soft bt-soft-sm", -+ "data-open-bot": botId, -+ onClick: () => onOpenBot?.(botId) -+ }, t("Open chat")) -+ ), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -+ ) -+ ); - } --function atSources({ roster, sessions, launched, iconFor }) { -- const bots = { -- trigger: "@", -- name: BOTS_SOURCE, -- order: -20, -- showGroupTitle: false, -- async candidates(session, { query }) { -- const snapshot = roster.getSnapshot(); -- const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); -- const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ -- // A friend's Bot by the one name the row inserts, "Chief (Bob)" (#967); its handle is the -- // second line (#384). A seat carries its own name; a friend's chat is named as its sidebar -- // row and header (#498). -- name: row.bot?.remote && !face ? row.bot.kind === "bot" ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? row.text.replace(/^@/, "").trimEnd() : t("Everyone"), -- ...row.bot?.role && !face ? { description: row.bot.role } : {}, -- ...row.remote ? { description: row.remote.handle } : {}, -- ...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 -- })); -- if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; -- const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ -- name: row.name, -- ...row.description ? { description: row.description } : {}, -- icon: row.icon ?? "session", -- section: row.section ?? "FlyBot", -- 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) } -+function projectAgentFor(bot, selfId) { -+ if (bot === null || typeof bot !== "object" || bot.remote !== void 0) return null; -+ const projectId = text(bot.projectId); -+ if (projectId === "") return null; -+ const owner = text(bot.ownerId); -+ const self = text(selfId); -+ if (self !== "" && owner !== "" && owner !== self) return null; -+ return { projectId }; -+} -+function ProjectAgentEntry({ projectId, read, onOpen } = {}) { -+ const [agentId, setAgentId] = (0, import_react19.useState)(""); -+ const id = text(projectId); -+ const open = async () => { -+ let target = agentId; -+ if (target === "") { -+ try { -+ const record = await (read ?? call)("project-read", { id }); -+ target = botIdOf(record?.project?.assistant); -+ } catch { -+ target = ""; -+ } -+ if (target !== "") setAgentId(target); -+ } -+ if (target !== "") onOpen?.(target); - }; -- return [bots, chats]; -+ if (id === "") return null; -+ return (0, import_react19.createElement)("button", { -+ type: "button", -+ className: "bt-project-agent-entry", -+ "data-project-agent": id, -+ "data-open-bot": agentId === "" ? void 0 : agentId, -+ title: t("Open my agent"), -+ "aria-label": t("Open my agent"), -+ onClick: open -+ }, t("My agent")); -+} -+function ProjectBoardPane({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers, onOpenBot, onOpenDossier }) { -+ const view = projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics }); -+ return (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, -+ (0, import_react19.createElement)("h3", null, view.title), -+ (0, import_react19.createElement)(ProjectTeam, { project, onOpenBot, onOpenDossier }), -+ // Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown. -+ view.counts === null ? null : (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" }, -+ (0, import_react19.createElement)("span", null, [t("Waiting"), ": ", (0, import_react19.createElement)("b", { key: "w" }, countText(view.counts.waiting))]), -+ (0, import_react19.createElement)("span", null, [t("In flight"), ": ", (0, import_react19.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), -+ (0, import_react19.createElement)("span", null, [t("Blocked"), ": ", (0, import_react19.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), -+ (0, import_react19.createElement)("span", null, [t("Done"), ": ", (0, import_react19.createElement)("b", { key: "d" }, countText(view.counts.done))]) -+ ), -+ // #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_react19.createElement)( -+ "div", -+ { className: "bt-project-actions" }, -+ (0, import_react19.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), -+ (0, import_react19.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) -+ ), -+ (0, import_react19.createElement)(ProjectMetrics, { view: view.metrics }), -+ (0, import_react19.createElement)(ProjectRunningTable, { view: view.metrics, onOpen }), -+ (0, import_react19.createElement)(ProjectBoardRows, { view, onOpen }), -+ (0, import_react19.createElement)(ProjectQueueGroups, { view, onOpen }), -+ (0, import_react19.createElement)(ProjectDoneGroup, { view, onOpen }), -+ (0, import_react19.createElement)(ProjectBoardRecord, { record: view.record }) -+ ); - } -- --// src/client/face-at-filter.js --var REFERENCE_SOURCE = "reference"; --var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; --var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; --var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; --function planFaceMenu(state, previous = null) { -- const hidden = []; -- const ready = []; -- let shown = 0; -- let loading = false; -- for (const group of state?.groups ?? []) { -- const items = group.items ?? []; -- if (group.status !== "ready" && items.length === 0) loading = true; -- items.forEach((item, index) => { -- const hide = isSessionRow(group, item); -- if (hide) hidden.push({ source: group.source, index, title: item.section !== void 0 && item.section !== items[index - 1]?.section }); -- else shown += 1; -- if (group.status === "ready") ready.push({ source: group.source, index, hide }); -- }); -- } -- const open = state?.open === true; -- const blank3 = open && hidden.length > 0 && shown === 0 && !loading; -- const dismiss = blank3 && (state.groups ?? []).every((group) => group.status === "ready"); -- return { hidden, blank: blank3, dismiss, target: dismiss ? null : steer(ready, state?.highlight ?? null, previous) }; -+function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { -+ const save = request ?? call; -+ const [kind, setKind] = (0, import_react19.useState)("bot"); -+ const [memberId, setMemberId] = (0, import_react19.useState)(""); -+ const [note, setNote] = (0, import_react19.useState)(""); -+ const [assistantOff, setAssistantOff] = (0, import_react19.useState)(assistantMine === void 0 ? project?.assistant?.off === true : assistantMine?.off === true); -+ const [status, setStatus] = (0, import_react19.useState)(""); -+ const id = text(project?.id); -+ const run = async (endpoint, payload, done) => { -+ try { -+ const result = await save(endpoint, payload); -+ setStatus(done); -+ onSaved?.(result); -+ return result; -+ } catch (error) { -+ setStatus(hostText(error?.message ?? String(error))); -+ return null; -+ } -+ }; -+ return (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-panel", "data-panel": "project-settings" }, -+ (0, import_react19.createElement)("h3", null, t("Project settings")), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ (0, import_react19.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)( -+ "select", -+ { "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) }, -+ (0, import_react19.createElement)("option", { value: "bot" }, t("Bot")), -+ (0, import_react19.createElement)("option", { value: "group" }, t("Group")) -+ ), -+ (0, import_react19.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind")) -+ ) -+ ), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ (0, import_react19.createElement)("label", null, t("Project Assistant")), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("label", null, (0, import_react19.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save")) -+ ) -+ ), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-field" }, -+ // The host knows two memory kinds; the free text the panel takes is recorded as a decision, -+ // never as a third kind the host would refuse (#1820). -+ (0, import_react19.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")), -+ (0, import_react19.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory")) -+ ) -+ ), -+ (0, import_react19.createElement)("div", { className: "bt-project-status-line", role: "status" }, status), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -+ ) -+ ); - } --function steer(ready, highlight, previous) { -- const at = ready.findIndex((row) => same(row, highlight)); -- if (at < 0 || !ready[at].hide) return null; -- const from = ready.findIndex((row) => same(row, previous)); -- const n = ready.length; -- const dir = from >= 0 && from !== at && (from - at + n) % n === 1 ? -1 : 1; -- for (let step = 1; step < n; step += 1) { -- const row = ready[((at + dir * step) % n + n) % n]; -- if (!row.hide) return { source: row.source, index: row.index }; -+function ProjectMembersPanel({ project, rows, onClose } = {}) { -+ const owners = []; -+ 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 = []; -+ for (const row of Array.isArray(rows) ? rows : []) { -+ const name = text(row?.assignee); -+ if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name); - } -- return null; --} --function faceMenuCss({ hidden, blank: blank3 }) { -- if (hidden.length === 0) return ""; -- const rows = hidden.map((row) => `[data-trigger-menu] [id="${optionId(row.source, row.index)}"]`); -- const titles = hidden.filter((row) => row.title).map((row) => `[data-trigger-menu] [role="presentation"]:has(+ [id="${optionId(row.source, row.index)}"])`); -- const css = [`${[...rows, ...titles].join(",")}{display:none!important}`]; -- if (blank3) css.push("[data-trigger-menu]{display:none!important}"); -- return css.join("\n"); -+ const chief = text(project?.chief?.botId); -+ return (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-panel", "data-panel": "project-members" }, -+ (0, import_react19.createElement)("h3", null, t("Members")), -+ (0, import_react19.createElement)( -+ "ul", -+ { className: "bt-project-members" }, -+ owners.map((owner) => (0, import_react19.createElement)( -+ "li", -+ { key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner }, -+ (0, import_react19.createElement)("span", null, `@${owner}`), -+ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("Owner")) -+ )), -+ chief === "" ? null : (0, import_react19.createElement)( -+ "li", -+ { key: `chief:${chief}`, className: "bt-project-member", "data-member-role": "chief", "data-member-id": chief }, -+ (0, import_react19.createElement)("span", null, t("Project Chief")), -+ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("Chief of this Project")) -+ ), -+ boardMembers.map((name) => (0, import_react19.createElement)( -+ "li", -+ { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, -+ (0, import_react19.createElement)("span", null, `@${name}`), -+ (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) -+ )) -+ ), -+ owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, -+ (0, import_react19.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.")), -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-project-row-actions" }, -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -+ ) -+ ); - } --function watchFaceMenu(controller, doc = globalThis.document) { -- if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { -+async function readProjectBoard(read, wanted) { -+ const listing = await read("projects"); -+ const projects = Array.isArray(listing?.projects) ? listing.projects : []; -+ const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); -+ if (project === void 0) return { project: null, rows: [], done: [], canManage: false, tasks: [], team: { rows: [] } }; -+ const zone = typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function" ? void 0 : Intl.DateTimeFormat().resolvedOptions().timeZone; -+ const [record, board, taskList, team] = await Promise.all([ -+ read("project-read", { id: project.id }), -+ read("project-board", { id: project.id, timeZone: zone }), -+ read("tasks", {}), -+ read("project-team", { id: project.id }) -+ ]); -+ const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; -+ 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 { -+ project: withRole, -+ assistantMine: record?.assistantMine ?? null, -+ rows: board?.tasks ?? [], -+ done: board?.done ?? [], -+ canManage: record?.canManage === true, -+ tasks, -+ decisions: board?.decisions, -+ meetings: board?.meetings, -+ queues, -+ metrics: board?.metrics ?? null, -+ team: team !== null && typeof team === "object" ? team : { rows: [] } - }; -- const style = doc.createElement("style"); -- style.setAttribute("data-bt-face-at", ""); -- doc.head.appendChild(style); -- let previous = null; -- let stopped = false; -- const later = typeof queueMicrotask === "function" ? queueMicrotask : (fn) => Promise.resolve().then(fn); -- const apply2 = () => { -- if (stopped) return; -- const state = controller.menu.getSnapshot(); -- const plan = planFaceMenu(state, previous); -- const css = faceMenuCss(plan); -- if (style.textContent !== css) style.textContent = css; -- if (plan.dismiss) later(() => { -- if (!stopped && planFaceMenu(controller.menu.getSnapshot(), previous).dismiss) controller.dismiss(); -- }); -- else if (plan.target) later(() => { -- if (!stopped) controller.hover(plan.target.source, plan.target.index); -- }); -- else previous = state.open ? state.highlight : null; -- }; -- const unsubscribe = controller.menu.subscribe(apply2); -- apply2(); -- return () => { -- stopped = true; -- unsubscribe?.(); -- style.remove(); -- }; --} -- --// src/client/speech.js --var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; --var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); --async function speechCatalog(speech) { -- const result = await speech.catalog(); -- if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); -- const catalog = result.value; -- const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); -- return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; --} --function waveBytes(samples, rate = 16e3) { -- const bytes = new Uint8Array(44 + samples.length * 2); -- const view = new DataView(bytes.buffer); -- const ascii = (at, text2) => { -- for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); -- }; -- ascii(0, "RIFF"); -- view.setUint32(4, 36 + samples.length * 2, true); -- ascii(8, "WAVEfmt "); -- view.setUint32(16, 16, true); -- view.setUint16(20, 1, true); -- view.setUint16(22, 1, true); -- view.setUint32(24, rate, true); -- view.setUint32(28, rate * 2, true); -- view.setUint16(32, 2, true); -- view.setUint16(34, 16, true); -- ascii(36, "data"); -- view.setUint32(40, samples.length * 2, true); -- samples.forEach((sample, index) => { -- const value = Math.max(-1, Math.min(1, sample)); -- view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); -- }); -- return bytes; --} --function base64Of(bytes) { -- let text2 = ""; -- for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); -- return btoa(text2); - } --var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); --async function openCapture() { -- 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); -- }; -- 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; -- } -+var PROJECT_BOARD_POLL_MS = 5e3; -+var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project"); -+function ProjectBoardOverlay({ actions, call: request } = {}) { -+ const read = request ?? call; -+ const [state, setState] = (0, import_react19.useState)(null); -+ const [panel, setPanel] = (0, import_react19.useState)(null); -+ const [dossier, setDossier] = (0, import_react19.useState)(null); -+ const [hash, setHash] = (0, import_react19.useState)(() => typeof window === "undefined" ? "" : window.location.hash); -+ const close = (0, import_react19.useCallback)(() => { -+ if (typeof window !== "undefined") window.location.hash = ""; -+ setPanel(null); -+ setState(null); -+ }, []); -+ (0, import_react19.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ const onKey = (event) => { -+ if (event.key !== "Escape") return; -+ if (panel !== null) setPanel(null); -+ else close(); -+ }; -+ window.addEventListener("keydown", onKey); -+ return () => window.removeEventListener("keydown", onKey); -+ }, [panel, close]); -+ (0, import_react19.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ const sync = () => setHash(window.location.hash); -+ window.addEventListener("hashchange", sync); -+ sync(); -+ return () => window.removeEventListener("hashchange", sync); -+ }, []); -+ (0, import_react19.useEffect)(() => { -+ if (typeof window === "undefined") return void 0; -+ if (!hash.startsWith("#project")) { -+ setState(null); -+ return void 0; -+ } -+ const wanted = hash.slice("#project".length).replace(/^=/, ""); -+ let gone = false; -+ const loadBoard = () => readProjectBoard(read, wanted); -+ const refresh = async (first) => { - try { -- await new Promise((resolve) => { -- recorder.onstop = resolve; -- recorder.stop(); -- }); -- stream.getTracks().forEach((track) => track.stop()); -- const blob = new Blob(chunks, { type: recorder.mimeType }); -- if (blob.size === 0) return null; -- const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); -- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); -- const offline = new OfflineAudioContext(1, length, 16e3); -- const source = offline.createBufferSource(); -- source.buffer = decoded; -- source.connect(offline.destination); -- source.start(); -- return waveBytes((await offline.startRendering()).getChannelData(0)); -- } finally { -- close(); -+ const next = await loadBoard(); -+ if (!gone) setState(next); -+ } catch { -+ if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); - } -- } -- }; --} --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"; -- }; --} -- --// src/client/model-menu.js --var import_react21 = 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 servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); --var effortCache = /* @__PURE__ */ new Map(); --function useEfforts(served, ref, actions) { -- const [fetched, setFetched] = (0, import_react21.useState)(void 0); -- (0, import_react21.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(() => { -- }); -+ }; -+ void refresh(true); -+ const timer = setInterval(() => { -+ if (!boardPollPaused()) void refresh(false); -+ }, PROJECT_BOARD_POLL_MS); - return () => { -- live = false; -+ gone = true; -+ clearInterval(timer); - }; -- }, [ref, served?.reasoning !== void 0, actions]); -- return served?.reasoning ?? fetched; --} --function effortRows(reasoning) { -- if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; -- return [ -- { effort: void 0, label: t("Default") }, -- ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) -- ]; --} --function EffortRows({ reasoning, current, onPick }) { -- return (0, import_react21.createElement)( -- import_react21.Fragment, -- null, -- (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -- effortRows(reasoning).map((row) => (0, import_react21.createElement)( -- "button", -- { -- key: row.effort ?? "default", -- type: "button", -- role: "menuitemradio", -- "aria-checked": row.effort === current, -- onClick: () => onPick(row.effort) -- }, -- (0, import_react21.createElement)("span", { className: "bt-menu-label" }, row.label), -- (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react21.createElement)(CheckIcon) : null) -- )) -- ); --} --function ModelRows({ roster, current, onPick }) { -- return groupModels(roster.models).map(([provider, list]) => (0, import_react21.createElement)( -- import_react21.Fragment, -- { key: list[0].provider ?? provider }, -- (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -- list.map((model) => (0, import_react21.createElement)( -- "button", -- { -- key: model.ref, -- type: "button", -- role: "menuitemradio", -- "aria-checked": model.ref === current, -- title: model.ref, -- onClick: () => onPick(model.ref) -- }, -- (0, import_react21.createElement)("span", { className: "bt-menu-label" }, model.name), -- model.ref === roster.defaultModel ? (0, import_react21.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -- (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react21.createElement)(CheckIcon) : null) -- )) -- )); --} --var Caret = () => (0, import_react21.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_react21.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); --function useAnchoredMenu() { -- const [place, setPlace] = (0, import_react21.useState)(null); -- const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); -- const button = (0, import_react21.useRef)(null); -- const menu = (0, import_react21.useRef)(null); -- const close = () => setPlace(null); -- (0, import_react21.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 }; --} --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; -- } -- void actions?.refreshModels?.(); -- open(); -- }; -- const pick2 = (ref) => { -- close(); -- if (onPick) onPick(ref); -- else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); -- }; -- const pickEffort = (effort) => { -- close(); -- if (onPickEffort) onPickEffort(effort); -- else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); -- }; -- const models = roster.models ?? []; -- return (0, import_react21.createElement)( -- import_react21.Fragment, -- null, -- (0, import_react21.createElement)( -- "button", -- { -- ref: button, -- type: "button", -- className: "bt-model-trigger", -- "data-wide": wide || void 0, -- "aria-haspopup": "menu", -- "aria-expanded": isOpen, -- "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), -- "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, -- onClick: toggle -- }, -- (0, import_react21.createElement)( -- "span", -- { className: "bt-model-copy" }, -- (0, import_react21.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -- shown ? (0, import_react21.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_react21.createElement)(Caret) -- ), -- shownPlace ? (0, import_react_dom8.createPortal)((0, import_react21.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_react21.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -- effortRows(reasoning).length > 0 ? (0, import_react21.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -- (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -- close(); -- openHarnessModels(); -- } }, (0, import_react21.createElement)(SlidersIcon), (0, import_react21.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_react21.useState)(""); -- const [error, setError] = (0, import_react21.useState)(""); -- const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; -- (0, import_react21.useEffect)(() => { -- 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_react21.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(""); -- void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { -- setError(failure?.message ?? String(failure)); -- void actions.refreshModels?.(); -- }).finally(() => setBusy("")); -- }; -- 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_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" })] : [ -- ...groupModels(models).map(([provider, list]) => (0, import_react21.createElement)( -- "div", -- { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, -- (0, import_react21.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -- (0, import_react21.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react21.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_react21.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -- )), -- (0, import_react21.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react21.createElement)(SlidersIcon), t("Manage models")) -- ]; -- return (0, import_react21.createElement)( -+ }, [hash]); -+ if (state === null) return null; -+ return (0, import_react19.createElement)( - "div", -- { className: "bt-model-dock" }, -- (0, import_react21.createElement)( -+ { className: "bt-project-overlay" }, -+ (0, import_react19.createElement)("div", { className: "bt-scrim", onClick: close }), -+ (0, import_react19.createElement)( - "div", -- { className: "bt-astack", "data-lead": "" }, -- (0, import_react21.createElement)( -+ { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") }, -+ (0, import_react19.createElement)( - "div", -- { className: "bt-msg" }, -- (0, import_react21.createElement)( -- "div", -- { className: "bt-msg-line" }, -- (0, import_react21.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react21.createElement)("div", { className: "bt-bubble" }, text2) -- ) -- ) -- ), -- (0, import_react21.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -- error ? (0, import_react21.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -- ); --} --function RemoteGoneDock({ bot }) { -- return (0, import_react21.createElement)( -- "div", -- { className: "bt-model-dock", "data-flybot": "remote-gone" }, -- (0, import_react21.createElement)( -- "div", -- { className: "bt-astack", "data-lead": "" }, -- (0, import_react21.createElement)( -+ { className: "bt-mem-head" }, -+ (0, import_react19.createElement)("h2", {}, t("Project board")), -+ (0, import_react19.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close")) -+ ), -+ (0, import_react19.createElement)( - "div", -- { className: "bt-msg" }, -- (0, import_react21.createElement)( -+ { className: "bt-project-body" }, -+ state.project === null ? (0, import_react19.createElement)( - "div", -- { className: "bt-msg-line" }, -- (0, import_react21.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react21.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 })) -- ) -+ { className: "bt-project-empty" }, -+ (0, import_react19.createElement)("p", null, t("No Project yet — create one.")), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ close(); -+ actions?.openSettings?.("projects"); -+ } }, t("Open Projects in Settings")) -+ ) : panel === "dossier" ? (0, import_react19.createElement)(ProjectDossierPanel, { -+ row: dossierRowOf(state.team, dossier?.botId), -+ label: dossier?.label ?? "", -+ onClose: () => setPanel(null), -+ onOpenBot: (id) => { -+ close(); -+ actions?.openSession?.(id); -+ } -+ }) : panel === "settings" ? (0, import_react19.createElement)(ProjectSettingsPanel, { project: state.project, assistantMine: state.assistantMine, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react19.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react19.createElement)(ProjectBoardPane, { -+ project: state.project, -+ rows: state.rows, -+ done: state.done, -+ canManage: state.canManage, -+ tasks: state.tasks, -+ decisions: state.decisions, -+ meetings: state.meetings, -+ queues: state.queues, -+ metrics: state.metrics, -+ team: state.team, -+ onSettings: () => setPanel("settings"), -+ onMembers: () => setPanel("members"), -+ // #1864: a team row's Dossier control opens the read-only panel, fed by the same read. -+ onOpenDossier: (value) => { -+ setDossier(value); -+ setPanel("dossier"); -+ }, -+ // #1860: a team row opens that Bot's chat. The board is a modal over the conversation, -+ // so it closes first — otherwise the chat opens behind it. -+ onOpenBot: (id) => { -+ close(); -+ actions?.openSession?.(id); -+ }, -+ // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and -+ // lands on that Bot or group. Nothing here re-decides which board that is. -+ onOpen: (board) => { -+ if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`; -+ actions?.openBoard?.(); -+ setState(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"), openHarnessModels] -- ].filter(Boolean); --} --var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; --function KeyHelpLink() { -- return (0, import_react21.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); --} --function missingModelKey(message) { -- const text2 = String(message ?? ""); -- if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; -- return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; --} --function modelKeyChoices(actions, route) { -- if (/^deepseek/i.test(route)) return setupChoices(actions); -- return [[t("Manage models"), openHarnessModels]]; -+ -+// src/client/header.js -+var import_react20 = require("react"); -+var import_react_dom6 = require("react-dom"); -+var GoArrow = () => (0, import_react20.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_react20.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_react20.useState)(false); -+ (0, import_react20.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; - } --function TurnErrorCell({ node, t: chatT, actions }) { -- const data = node?.data ?? {}; -- const code = typeof data.code === "string" && data.code ? data.code : void 0; -- const raw = String(data.message ?? ""); -- const shell = (key, fallback) => { -- const text2 = typeof chatT === "function" ? chatT(key) : void 0; -- return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; -- }; -- const missing = missingModelKey(raw); -- const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); -- const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; -- const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; -- const deepseek = missing ? /^deepseek/i.test(missing.route) : false; -- return (0, import_react21.createElement)( -- "div", -- { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, -- (0, import_react21.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), -- (0, import_react21.createElement)( -- "div", -- { className: "bt-turn-error-copy" }, -- (0, import_react21.createElement)("span", { className: "bt-turn-error-title" }, title), -- (0, import_react21.createElement)("span", { className: "bt-turn-error-message" }, message), -- code ? (0, import_react21.createElement)("code", { className: "bt-turn-error-code" }, code) : null -- ), -- choices.length > 0 ? (0, import_react21.createElement)( -- "div", -- { className: "bt-turn-error-actions" }, -- choices.map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), -- deepseek ? (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" }) : null -- ) : null -+function MemoryNotice({ shown }) { -+ const inner = (0, import_react20.useRef)(null); -+ const [width, setWidth] = (0, import_react20.useState)(0); -+ const label = t("Memory updated"); -+ (0, import_react20.useLayoutEffect)(() => { -+ if (shown) setWidth(inner.current?.scrollWidth ?? 0); -+ }, [label, shown]); -+ return (0, import_react20.createElement)( -+ "span", -+ { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, -+ (0, import_react20.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react20.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react20.createElement)(MemoryIcon)), label) - ); - } --function DeepSeekSignIn({ useUi, actions }) { -- const open = useUi((state) => state.signIn); -- const entry = open ? actions.signInEntry?.() : null; -- if (!entry) return null; -- return (0, import_react21.createElement)(SignInHost, { key: open, entry, actions }); --} --function SignInHost({ entry, actions }) { -- const [ops] = (0, import_react21.useState)(() => entry.inject?.() ?? {}); -- const close = () => actions.closeSignIn(); -- const useHook = (source) => (selector) => (0, import_react21.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -- const [hooks] = (0, import_react21.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -- return (0, import_react21.createElement)(entry.component, { -- ...ops, -- ...hooks, -- t: actions.accountT(), -- complete: close, -- useApiKey: () => { -- close(); -- openHarnessOnboarding("deepseek-official"); -- } -+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(",") : ""; - }); --} --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} --.bt-turn-error-copy{min-width:0;overflow-wrap:anywhere} --.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135} --.bt-turn-error-message{color:var(--bt-ink-2)} --.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} --.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px} --.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px} --.bt-model-help:hover{color:var(--bt-ink)} --.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px} --.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/model-test.js --var import_react22 = require("react"); --var FAILURE_COPY = { -- NO_MODEL: "Pick a model first.", -- NO_KEY: "This model has no API key yet. Set one up in the model settings.", -- AUTH: "The API key was rejected. Check it in the model settings.", -- QUOTA: "The account is out of quota.", -- RATE_LIMIT: "Too many requests right now. Try again in a moment.", -- TIMEOUT: "The model did not answer within 30 seconds.", -- NOT_SERVED: "This model is not available now. Check its provider in the model settings.", -- MODEL_NOT_FOUND: "The provider does not know this model. Check the model name.", -- TRANSPORT: "Could not reach the model service. Check the endpoint address and your network.", -- SERVER: "The model service failed. Try again later.", -- INVALID_REQUEST: "The model service refused the request. Check the endpoint and the model.", -- UNKNOWN: "The test failed." --}; --function testOutcome(result, modelName) { -- if (result?.ok === true) { -- const seconds = typeof result.ms === "number" ? Math.max(0.1, Math.round(result.ms / 100) / 10) : void 0; -- return { ok: true, text: seconds === void 0 ? t("Works. {model} answered.", { model: modelName }) : t("Works. {model} answered in {seconds} s.", { model: modelName, seconds }) }; -- } -- const code = Object.hasOwn(FAILURE_COPY, result?.code) ? result.code : "UNKNOWN"; -- return { ok: false, code, text: t(FAILURE_COPY[code]) }; --} --var testModel = (id) => call("test-model", { id }); --function ModelTestRow({ bot, modelName, test = testModel }) { -- const [state, setState] = (0, import_react22.useState)({ busy: false, outcome: void 0 }); -- const live = (0, import_react22.useRef)(true); -- (0, import_react22.useEffect)(() => () => { -- live.current = false; -- }, []); -- (0, import_react22.useEffect)(() => { -- setState({ busy: false, outcome: void 0 }); -- }, [bot.id, bot.model]); -- const start = () => { -- if (state.busy) return; -- setState({ busy: true, outcome: void 0 }); -- Promise.resolve().then(() => test(bot.id)).then( -- (result) => testOutcome(result, modelName ?? result?.ref ?? ""), -- (error) => ({ ok: false, text: String(error?.message ?? t("The test failed.")) }) -- ).then((outcome2) => { -- if (live.current) setState({ busy: false, outcome: outcome2 }); -- }); -- }; -- const { busy, outcome } = state; -- return (0, import_react22.createElement)( -+ const hiddenTurns = roster.exchangeTurns?.[sessionId]; -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const opening = useTurnClock((state) => { -+ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); -+ return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -+ }); -+ (0, import_react20.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 projectAgent = projectAgentFor(bot, roster.self?.memberId); -+ const room2 = roster.roomsById[sessionId]; -+ const detailsOpen = useUi((state) => state.details === sessionId); -+ const full2 = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; -+ const face = room2?.origin === "face-to-face"; -+ const name = face ? faceRowTitle(full2) : full2; -+ const useNews = useMemoryNews ?? noNews; -+ const memoryIds = bot ? [bot.id] : room2?.members ?? []; -+ const notice = useMemoryNotice(memoryIds, useNews); -+ const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); -+ const memoryNotice = notice && changed2 !== null && roster.memory !== false; -+ return (0, import_react20.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-model-test" }, -- (0, import_react22.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), -- (0, import_react22.createElement)( -- "span", -- { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, -- busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, -- outcome?.code ? (0, import_react22.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null -- ) -+ { className: "bt-head" }, -+ (0, import_react20.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[changed2]?.name ?? full2 }) : t("View conversation details"), -+ onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId) -+ }, -+ bot ? (0, import_react20.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react20.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -+ (0, import_react20.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_react20.createElement)(MemoryNotice, { shown: memoryNotice }), -+ (0, import_react20.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react20.createElement)(GoArrow)) - ), -- (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) -+ (0, import_react20.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -+ projectAgent === null ? null : (0, import_react20.createElement)(ProjectAgentEntry, { -+ projectId: projectAgent.projectId, -+ read: call, -+ onOpen: (id) => actions?.openSession?.(id) -+ }), -+ (0, import_react20.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), -+ (0, import_react20.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), -+ (0, import_react20.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) - ); - } --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} --.bt-model-test-result[data-ok=true]{color:#1a7f37} --.bt-model-test-result[data-ok=false]{color:#c21d2e} --.bt-model-test-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} --/* #1347: the hint beside "Test connection" is prose and wraps; the button next to it keeps its own -- words ("Test connection" on one line) and takes the room it needs. */ --.bt-model-test>.bt-set-copy{flex:1 1 12ch;min-width:12ch} --.bt-model-test>.bt-soft{flex:none;white-space:nowrap} --@container bt-panel (max-width:300px){ -- .bt-model-test{flex-wrap:wrap;row-gap:6px} -- .bt-model-test>.bt-set-copy{flex:1 1 100%} -+function useTranscriptView() { -+ const [view, setView] = (0, import_react20.useState)(null); -+ (0, import_react20.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 () => { -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ document.removeEventListener("scroll", schedule, true); -+ window.removeEventListener("resize", schedule); -+ }; -+ }, []); -+ return view; - } --`; -- --// src/client/folder-menu.js --var import_react23 = require("react"); --var import_react_dom9 = require("react-dom"); --var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); --var Tick = ({ on }) => (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react23.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_react23.useState)(null); -- const [view, setView] = (0, import_react23.useState)("list"); -- const [draft, setDraft] = (0, import_react23.useState)(""); -- const [listing, setListing] = (0, import_react23.useState)(null); -- const [problem, setProblem] = (0, import_react23.useState)(""); -- const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); -- const toggle = () => { -- if (isOpen) { -- close(); -- return; -+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; - } -- 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_react23.createElement)( -+ return count; -+ }); -+ const view = useTranscriptView(); -+ const unpinned = view?.unpinned === true; -+ const [release, setRelease] = (0, import_react20.useState)({ base: null, dismissed: false }); -+ const [above, setAbove] = (0, import_react20.useState)({ seen: false, dismissed: false }); -+ (0, import_react20.useEffect)(() => { -+ setRelease({ base: unpinned ? replies : null, dismissed: false }); -+ }, [unpinned]); -+ (0, import_react20.useEffect)(() => { -+ if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); -+ }, [view?.divider]); -+ const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); -+ (0, import_react20.useEffect)(() => { -+ if (above.seen && movedOn) actions?.settleVisit?.(sessionId); -+ }, [above.seen, movedOn, sessionId]); -+ const fresh = release.base === null ? 0 : replies - release.base; -+ const showDown = unpinned && fresh > 0 && !release.dismissed; -+ const showUp = view?.divider === "above" && !above.seen && !above.dismissed; -+ (0, import_react20.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_react20.createElement)( - "div", -- { key: "list", className: "bt-folder-view" }, -- (0, import_react23.createElement)( -+ { className: "bt-news", "data-direction": direction, style }, -+ (0, import_react20.createElement)( - "button", -- { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, -- (0, import_react23.createElement)("span", { className: "bt-menu-label" }, fallback), -- (0, import_react23.createElement)(Tick, { on: !value }) -+ { type: "button", className: "bt-news-jump", onClick: onJump }, -+ (0, import_react20.createElement)("span", { className: "bt-news-ico" }, (0, import_react20.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -+ label(count) - ), -- folders.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -- folders.map((folder) => (0, import_react23.createElement)( -- "button", -- { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, -- (0, import_react23.createElement)("span", { className: "bt-folder-copy" }, (0, import_react23.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react23.createElement)("span", { className: "bt-folder-path" }, folder.path)), -- (0, import_react23.createElement)(Tick, { on: folder.path === value }) -- )), -- (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- info?.picker === "native" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -- info?.picker === "browse" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -- (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -- setDraft(value ?? ""); -- setView("type"); -- } }, (0, import_react23.createElement)(PencilIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) -- ); -- const typeView = (0, import_react23.createElement)( -- "div", -- { key: "type", className: "bt-folder-view" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react23.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_react20.createElement)("button", { -+ type: "button", -+ className: "bt-news-x", -+ "aria-label": t("Dismiss new messages"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onDismiss(); - } -- }), -- (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) -+ }, (0, import_react20.createElement)(CloseIcon)) - ); -- 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_react23.createElement)( -- "div", -- { key: `browse:${listing.path}`, className: "bt-folder-view" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -- parent ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -- inside.map((entry) => (0, import_react23.createElement)( -- "button", -- { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, -- (0, import_react23.createElement)(FolderIcon), -- (0, import_react23.createElement)("span", { className: "bt-menu-label" }, entry.name), -- (0, import_react23.createElement)(ChevronRightIcon) -- )), -- inside.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -- listing.truncated ? (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -- (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react23.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_react23.createElement)( -- import_react23.Fragment, -- null, -- (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("Working folder: {folder}", { folder: value ?? fallback }), -- title: value, -- onClick: toggle -- }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-model-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -- value ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, value) : null -- ), -- (0, import_react23.createElement)(Caret) -+ const toLatest = () => { -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); -+ if (back) back.click(); -+ else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); -+ }; -+ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); -+ return (0, import_react_dom6.createPortal)( -+ (0, import_react20.createElement)( -+ import_react20.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 - ), -- shownPlace ? (0, import_react_dom9.createPortal)((0, import_react23.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_react23.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null -- ), document.body) : null -+ document.body - ); - } --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_react24 = require("react"); --var POLL_MS4 = 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_react24.useState)(shared); -- (0, import_react24.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_MS4); -- }); -- load(); -- return () => { -- stopped = true; -- clearTimeout(timer); -- listeners.delete(setView); -- }; -- }, []); -- return view; --} --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 weekdayNames(days) { -- const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); -- return names.join(dateLocale().startsWith("zh") ? "、" : ", "); --} --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 }); -- if (seconds % 60 === 0) return t("Every {count} minutes", { count: seconds / 60 }); -- return t("Every {count} seconds", { count: seconds }); --} --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 text2 = 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 `${text2}${zone}`; -- } -- case "cron": -- return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -- default: -- return ""; -- } --} --var nextText = (task) => task.status === "paused" ? t("Paused") : task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); --function lastRunText(run) { -- if (run === void 0 || !Number.isFinite(run.at)) return ""; -- const time = dayAndTime(run.at); -- switch (run.result) { -- case "running": -- return t("Running since {time}", { time }); -- case "stopped": -- return t("Last run {time} · Stopped", { time }); -- case "failed": -- return t("Last run {time} · Failed: {reason}", { time, reason: hostText(run.reason || "Unknown error") }); -- default: -- return t("Last run {time}", { time }); -- } --} --function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) { -- const [confirming, setConfirming] = (0, import_react24.useState)(false); -- const [busy, setBusy] = (0, import_react24.useState)(false); -- (0, import_react24.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 3e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const remove = () => { -- if (!confirming) { -- setConfirming(true); -- return; -+// src/client/composer-label.js -+var ATTRIBUTES = ["aria-label", "data-placeholder"]; -+function holdComposerLabel(doc, label) { -+ doc.body.style.setProperty("--bt-placeholder", JSON.stringify(label)); -+ const shell = /* @__PURE__ */ new Map(); -+ const apply2 = () => { -+ for (const el of doc.querySelectorAll("[data-composer-input]")) { -+ for (const name of ATTRIBUTES) { -+ const current = el.getAttribute(name); -+ if (current === label) continue; -+ if (!shell.has(el)) shell.set(el, {}); -+ shell.get(el)[name] = current; -+ el.setAttribute(name, label); -+ } - } -- setBusy(true); -- void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); - }; -- const toggle = () => { -- setBusy(true); -- const step = task.status === "paused" ? actions.scheduleResume : actions.schedulePause; -- void step(task.sessionId, task.id).then((next) => { -- setBusy(false); -- onView(next); -- }, () => setBusy(false)); -+ apply2(); -+ const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver; -+ const watcher = typeof Observer === "function" ? new Observer(apply2) : null; -+ watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES }); -+ return () => { -+ watcher?.disconnect(); -+ for (const [el, values] of shell) { -+ if (!el.isConnected) continue; -+ for (const [name, value] of Object.entries(values)) { -+ if (value == null) el.removeAttribute(name); -+ else el.setAttribute(name, value); -+ } -+ } - }; -- const [retryError, setRetryError] = (0, import_react24.useState)(""); -- const retry = () => { -- setBusy(true); -- setRetryError(""); -- void actions.scheduleRetry(task.sessionId, task.id).then((next) => { -- setBusy(false); -- onView(next); -- }, (error) => { -- setBusy(false); -- setRetryError(hostText(error?.message ?? String(error))); -+} -+ -+// src/client/header-effects.js -+function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { -+ if (!doc) return () => { -+ }; -+ const slots = /* @__PURE__ */ new Map(); -+ const run = (id, dep, setup) => { -+ const held = slots.get(id); -+ if (held !== void 0 && held.dep === dep) return; -+ held?.off(); -+ slots.set(id, { dep, off: setup() ?? (() => { -+ }) }); -+ }; -+ let running = false; -+ let again = false; -+ const pass = () => { -+ const list = sessions.getSnapshot(); -+ const state = roster.getSnapshot(); -+ const row = Object.values(list?.byId ?? {}).find((entry) => (entry?.retainedBy?.mainView ?? 0) > 0); -+ const sessionId = row?.id; -+ const bot = sessionId === void 0 ? void 0 : state?.byId?.[sessionId]; -+ const room2 = sessionId === void 0 ? void 0 : state?.roomsById?.[sessionId]; -+ const name = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; -+ run("friend-chat", `${sessionId}|${bot?.remote === true}`, () => { -+ if (!bot?.remote) return void 0; -+ doc.body.dataset.btFriendChat = ""; -+ return () => { -+ delete doc.body.dataset.btFriendChat; -+ }; -+ }); -+ run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { -+ if (!bot?.remote || row?.blank !== true) return void 0; -+ doc.body.dataset.btRemoteChat = "empty"; -+ doc.body.style.setProperty("--bt-remote-empty", JSON.stringify(t("Send a message. Your friend approves unless Auto-send is on."))); -+ return () => { -+ delete doc.body.dataset.btRemoteChat; -+ doc.body.style.removeProperty("--bt-remote-empty"); -+ }; -+ }); -+ run("composer-label", `${sessionId}|${name}`, () => holdComposerLabel(doc, name ? t("Message {name}", { name }) : t("Message"))); -+ run("visit", sessionId, () => { -+ if (sessionId === void 0) return void 0; -+ actions.beginVisit(sessionId); -+ return () => actions.endVisit(sessionId); -+ }); -+ run("face-at-menu", `${sessionId}|${room2?.origin === "face-to-face"}`, () => { -+ if (room2?.origin !== "face-to-face") return void 0; -+ const stop = actions.filterFaceAtMenu?.(sessionId); -+ return typeof stop === "function" ? stop : void 0; -+ }); -+ run("room-flag", `${sessionId}|${room2 !== void 0}`, () => { -+ if (room2 === void 0) return void 0; -+ doc.body.dataset.btRoom = ""; -+ return () => { -+ delete doc.body.dataset.btRoom; -+ }; - }); - }; -- const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; -- const stoppable = task.status === "active" || task.status === "paused"; -- const failed = task.lastRun?.result === "failed"; -- const last = lastRunText(task.lastRun); -- return (0, import_react24.createElement)( -- "div", -- { className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, -- showBot ? (0, import_react24.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -- (0, import_react24.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react24.createElement)("span", { className: "bt-task-title" }, task.title), -- (0, import_react24.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_react24.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, -- last ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, -- retryError ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null -- ), -- (0, import_react24.createElement)("span", { className: "bt-task-next" }, nextText(task)), -- failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, -- onEdit ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react24.createElement)(PencilIcon)) : null, -- onCopy ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react24.createElement)(DuplicateIcon)) : null, -- stoppable ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, -- (0, import_react24.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_react24.createElement)(TrashIcon)) -- ); --} --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 = [["seconds", 1, "seconds"], ["minutes", 60, "minutes"], ["hours", 3600, "hours"], ["days", 86400, "days"]]; --function localInput(ms) { -- const date = new Date(ms); -- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; --} --function formStart(task) { -- const seconds = task?.kind === "every" ? Number(task.everySeconds) : 3600; -- const [unit, size] = seconds % 86400 === 0 ? ["days", 86400] : seconds % 3600 === 0 ? ["hours", 3600] : seconds % 60 === 0 ? ["minutes", 60] : ["seconds", 1]; -- const once = task?.kind === "at" || task?.kind === "after"; -- return { -- title: task?.title ?? "", -- prompt: task?.prompt ?? "", -- kind: once ? "at" : task?.kind ?? "daily", -- at: once && !Number.isNaN(Date.parse(task.scheduledAt)) ? localInput(Date.parse(task.scheduledAt)) : inAnHour(), -- time: task?.time ? clock2(task.time) : "09:00", -- weekdays: task?.kind === "weekly" && Array.isArray(task.weekdays) ? task.weekdays : [1, 2, 3, 4, 5], -- count: Math.max(1, Math.round(seconds / size)), -- unit, -- timeZone: task?.timeZone || localZone() -+ const apply2 = () => { -+ if (running) { -+ again = true; -+ return; -+ } -+ running = true; -+ try { -+ pass(); -+ } finally { -+ running = false; -+ } -+ if (again) { -+ again = false; -+ apply2(); -+ } - }; --} --function TaskForm({ roster, actions, start, task, onDone, onCancel }) { -- const first = formStart(task); -- const [target, setTarget] = (0, import_react24.useState)(task ? task.botId ?? task.roomId : start ?? ""); -- const [title, setTitle] = (0, import_react24.useState)(first.title); -- const [prompt, setPrompt] = (0, import_react24.useState)(first.prompt); -- const [kind, setKind] = (0, import_react24.useState)(first.kind); -- const [at, setAt] = (0, import_react24.useState)(first.at); -- const [time, setTime] = (0, import_react24.useState)(first.time); -- const [weekdays, setWeekdays] = (0, import_react24.useState)(first.weekdays); -- const [count, setCount] = (0, import_react24.useState)(first.count); -- const [unit, setUnit] = (0, import_react24.useState)(first.unit); -- const [busy, setBusy] = (0, import_react24.useState)(false); -- const [error, setError] = (0, import_react24.useState)(""); -- const timing = () => { -- if (kind === "cron") return { kind: "keep" }; -- if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone }; -- if (kind === "every") return { kind, seconds: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; -- return { kind, time, timeZone: first.timeZone, ...kind === "weekly" ? { weekdays } : {} }; -+ apply2(); -+ const offSessions = sessions.subscribe(apply2); -+ const offRoster = roster.subscribe(apply2); -+ return () => { -+ offSessions?.(); -+ offRoster?.(); -+ for (const held of slots.values()) held.off(); -+ slots.clear(); - }; -- const submit = (event) => { -- event.preventDefault(); -- if (busy || target === "") return; -- setBusy(true); -- setError(""); -- let when3; -+} -+ -+// src/client/composer.js -+var import_react21 = require("react"); -+var import_react_dom7 = require("react-dom"); -+var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; -+var keepComposerFocus = (event) => event.preventDefault(); -+function useDictation(onText) { -+ const [listening, setListening] = (0, import_react21.useState)(false); -+ const [note, setNote] = (0, import_react21.useState)(""); -+ const recognition = (0, import_react21.useRef)(null); -+ const latest = (0, import_react21.useRef)(onText); -+ latest.current = onText; -+ (0, import_react21.useEffect)(() => () => recognition.current?.abort(), []); -+ (0, import_react21.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]; -+ const text2 = result[0]?.transcript ?? ""; -+ if (result.isFinal) { -+ 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 { -- when3 = timing(); -+ engine.start(); -+ setListening(true); -+ return true; - } catch { -- setError(t("Pick a date and time")); -- setBusy(false); -- return; -+ setNote(t("Couldn't start dictation")); -+ return false; - } -- const saved = task ? actions.scheduleUpdate({ sessionId: task.sessionId, id: task.id, target, title, prompt, when: when3 }) : actions.scheduleCreate({ target, title, prompt, when: when3 }); -- void saved.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_react24.createElement)( -- "form", -- { className: "bt-set-card bt-task-form", onSubmit: submit }, -- (0, import_react24.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -- (0, import_react24.createElement)( -- "select", -- { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, -- (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -- (0, import_react24.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react24.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -- roster.rooms.length > 0 ? (0, import_react24.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react24.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null -- ) -- ), -- (0, import_react24.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react24.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) -- ), -- (0, import_react24.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -- (0, import_react24.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_react24.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("When"))), -- (0, import_react24.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -- (0, import_react24.createElement)( -- "div", -- { key: kind, className: "bt-task-when bt-veil" }, -- kind === "cron" ? (0, import_react24.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, -- kind === "at" ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react24.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_react24.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_react24.createElement)( -- import_react24.Fragment, -- null, -- (0, import_react24.createElement)("span", { className: "bt-task-every" }, t("Every")), -- (0, import_react24.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_react24.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_react24.createElement)("option", { key: id, value: id }, t(label))) -- ) -- ) : null -- ) -- ), -- error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -- (0, import_react24.createElement)( -- "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react24.createElement)( -- "button", -- { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, -- task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") -- ) -- ) -- ); --} --function openPlugins(actions) { -- actions.closeSettings(); -- actions.selectPanel("plugins"); --} --function AutomationOff({ actions }) { -- return (0, import_react24.createElement)( -- "div", -- { className: "bt-set-card bt-task-off" }, -- (0, import_react24.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react24.createElement)(ClockIcon)), -- (0, import_react24.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -- (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), -- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) -- ); -+ const stop = () => recognition.current?.stop(); -+ return { listening, note, start, stop }; - } --function QuietHoursCard({ view, actions }) { -- const quiet = view.quietHours ?? { on: false }; -- const [busy, setBusy] = (0, import_react24.useState)(false); -- const [error, setError] = (0, import_react24.useState)(""); -- const [from, setFrom] = (0, import_react24.useState)(quiet.from ?? "22:00"); -- const [to, setTo] = (0, import_react24.useState)(quiet.to ?? "07:00"); -- const unchecked = view.quietUnchecked === true; -- const refresh = () => { -- void Promise.resolve(actions.schedules?.()).then((next) => { -- if (next) publish(next); -- }).catch(() => { -- }); -- }; -- const save = (next) => { -- if (busy) return; -- setBusy(true); -- setError(""); -- const shown = next.on ? { on: true, from: next.from ?? from, to: next.to ?? to } : { on: false }; -- void Promise.resolve(actions.setPrefs({ quietHours: next })).then(() => { -- publish({ ...view, quietHours: shown }); -- refresh(); -- }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); -- }; -- return (0, import_react24.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react24.createElement)("h3", null, t("Quiet hours")), -- (0, import_react24.createElement)( -- "div", -- { className: "bt-set-card" }, -- (0, import_react24.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react24.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react24.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), -- (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) -- ), -- (0, import_react24.createElement)("button", { -- type: "button", -- className: "bt-soft bt-soft-sm", -- disabled: busy, -- "aria-pressed": quiet.on, -- onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) -- }, quiet.on ? t("Turn off") : t("Turn on")) -- ), -- quiet.on ? (0, import_react24.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Window"))), -- (0, import_react24.createElement)( -- "div", -- { className: "bt-task-when" }, -- (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), -- (0, import_react24.createElement)("span", { className: "bt-task-every" }, "–"), -- (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), -- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) -- ) -- ) : null, -- error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -- quiet.on && unchecked ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null -- ) -+function ComposerIdentity({ sessionId, useRoster }) { -+ const roster = useRoster((value) => value); -+ const room2 = roster.roomsById?.[sessionId]; -+ const self = roster.self; -+ const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ if (room2?.viewerRole === "invited-owner") { -+ return (0, import_react21.createElement)( -+ "span", -+ { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, -+ t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) -+ ); -+ } -+ if (!self || !room2 || humans.length < 2) return null; -+ return (0, import_react21.createElement)( -+ "span", -+ { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, -+ t("Sending as {name}", { name: self.displayName || self.memberId }) - ); - } --function SchedulesPage({ roster, actions, start }) { -- const view = useSchedules(actions); -- const [adding, setAdding] = (0, import_react24.useState)(Boolean(start)); -- const [editing, setEditing] = (0, import_react24.useState)(null); -- if (view === null) return (0, import_react24.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -- if (!view.available) return (0, import_react24.createElement)(AutomationOff, { actions }); -- const shown = (next) => { -- publish(next); -- setAdding(false); -- setEditing(null); -- }; -- const edit = (task) => { -- setAdding(false); -- setEditing(task); -- }; -- const byChat = [ -- ...roster.bots.map((bot) => [bot.id, (0, import_react24.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_react24.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 [ -- (0, import_react24.createElement)(QuietHoursCard, { key: "quiet", view, actions }), -- adding ? (0, import_react24.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react24.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { -- setEditing(null); -- setAdding(true); -- } }, (0, import_react24.createElement)(PlusIcon), t("New task"))), -- byChat.length === 0 && !adding ? (0, import_react24.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react24.createElement)( -- "section", -- { key: id, className: "bt-set-section" }, -- (0, import_react24.createElement)("h3", { className: "bt-task-head" }, avatar, name), -- (0, import_react24.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) -- )) -- ]; --} --function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { -- const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; -- const [copying, setCopying] = (0, import_react24.useState)(null); -- const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0; -- const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react24.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 })); -- return (0, import_react24.createElement)( -- import_react24.Fragment, -+function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -+ const dictation = useDictation((text2, final) => { -+ if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); -+ }); -+ const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -+ (0, import_react21.useEffect)(() => { -+ if (!quote || !inputActions) return; -+ actions.clearQuote(quote.token); -+ const excerpt = quote.text.replace(/\s+/g, " ").trim(); -+ const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} -+ -+`; -+ inputActions.insertText(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; -+ return (0, import_react21.createElement)( -+ import_react21.Fragment, - null, -- open ? (0, import_react24.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, -- source ? (0, import_react24.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => { -- setCopying(null); -- onDone(next); -- }, onCancel: () => setCopying(null) }) : null, -- rows.length + extra.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null -+ pluginMic ? null : (0, import_react21.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_react21.createElement)(MicIcon)), -+ (0, import_react21.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_react21.createElement)(WaveIcon, { size: 18 })), -+ dictation.note ? (0, import_react21.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null - ); - } --function CopyRow({ task, roster, actions, onView, onCancel }) { -- const others = [ -- ...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]), -- ...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name]) -- ]; -- const [target, setTarget] = (0, import_react24.useState)(""); -- const [busy, setBusy] = (0, import_react24.useState)(false); -- const [error, setError] = (0, import_react24.useState)(""); -- const submit = (event) => { -- event.preventDefault(); -- if (busy || target === "") return; -- setBusy(true); -- setError(""); -- void actions.scheduleCreate({ target, copyFrom: { sessionId: task.sessionId, id: task.id } }).then(onView, (failure) => { -- setBusy(false); -- setError(hostText(failure?.message ?? String(failure))); -- }); -+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_react21.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); -+ return marks.get(id); - }; -- return (0, import_react24.createElement)( -- "form", -- { className: "bt-set-row bt-task-copy", onSubmit: submit }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))), -- (0, import_react24.createElement)( -- "select", -- { className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) }, -- (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -- others.map(([id, name]) => (0, import_react24.createElement)("option", { key: id, value: id }, name)) -- ), -- (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")), -- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- error ? (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null -- ); - } --function BotSchedules({ bot, roster, actions }) { -- const view = useSchedules(actions); -- const [editing, setEditing] = (0, import_react24.useState)(null); -- if (view === null) return null; -- const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; -- return (0, import_react24.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react24.createElement)("h3", null, t("Scheduled tasks")), -- !view.available ? (0, import_react24.createElement)( -+function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { -+ const roster = useRoster((value) => value); -+ const [open, setOpen] = (0, import_react21.useState)(null); -+ const [shown, leaving] = useLinger(open, 120); -+ const button = (0, import_react21.useRef)(null); -+ const menu = (0, import_react21.useRef)(null); -+ (0, import_react21.useEffect)(() => { -+ if (!open) return void 0; -+ const onDown = (event) => { -+ if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ setOpen(null); -+ } -+ }; -+ window.addEventListener("mousedown", onDown, true); -+ window.addEventListener("keydown", onKey, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown, true); -+ window.removeEventListener("keydown", onKey, true); -+ }; -+ }, [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 }); -+ }; -+ const attach = () => { -+ setOpen(null); -+ button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); -+ }; -+ const schedule = () => { -+ setOpen(null); -+ actions.openSettings("schedules", target); -+ }; -+ const reference = () => { -+ setOpen(null); -+ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); -+ }; -+ const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); -+ return (0, import_react21.createElement)( -+ import_react21.Fragment, -+ null, -+ (0, import_react21.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_react21.createElement)(PlusIcon)), -+ shown ? (0, import_react_dom7.createPortal)((0, import_react21.createElement)( - "div", -- { className: "bt-set-card" }, -- (0, import_react24.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), -- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) -- ) -- ) : (0, import_react24.createElement)(TaskList, { -- tasks, -- editing, -- roster, -- actions, -- onEdit: setEditing, -- onDone: (next) => { -- publish(next); -- setEditing(null); -+ { -+ 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 - }, -- onCancel: () => setEditing(null), -- extra: [(0, import_react24.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react24.createElement)(PlusIcon), t("New task"))] -- }) -+ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react21.createElement)(PaperclipIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -+ canReference ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react21.createElement)(ChatsIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -+ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react21.createElement)(ClockIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -+ ), document.body) : null - ); - } --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_react24.createElement)( -- "div", -- null, -- (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), -- tasks.length === 0 ? (0, import_react24.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.") : t("No scheduled tasks yet. Add one")) : (0, import_react24.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react24.createElement)( -- "button", -- { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, -- (0, import_react24.createElement)(ClockIcon), -- (0, import_react24.createElement)("span", { className: "bt-task-line-title" }, task.title), -- (0, import_react24.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) -- ))) -- ); --} --var SCHEDULE_CSS = ` --.bt-task{gap:10px} --.bt-task-copy{flex-wrap:wrap;gap:8px} --.bt-task-copy .bt-task-bot{flex:1;min-width:140px;max-width:100%} --.bt-task[data-done]{opacity:.6} --.bt-task[data-busy]{opacity:.4} --.bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} --.bt-task-prompt{font-size:12px;line-height:16px;color:var(--bt-ink-2);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} --.bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} --.bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} --.bt-task-remove svg{width:14px;height:14px} --.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} --.bt-task-tool svg{width:14px;height:14px} --.bt-task-pause{height:26px;color:var(--bt-ink-2)} --.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere} --.bt-task-last[data-failed]{color:#c21d2e} --.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)} --.bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} --.bt-task-head{display:flex;align-items:center;gap:6px} --.bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} --.bt-task-bot{width:auto;min-width:160px;max-width:58%} --.bt-task-when{display:flex;align-items:center;flex-wrap:wrap;gap:8px} --.bt-task-when .bt-input{width:auto;height:32px} --.bt-task-time{min-width:110px} --.bt-task-count{width:72px!important} --.bt-task-unit{width:auto;height:32px} --.bt-task-every{font-size:13px;color:var(--bt-ink-2)} --.bt-task-days{display:flex;gap:4px} --.bt-task-day{width:30px;height:30px;border-radius:50%;border:1px solid var(--bt-line);background:none;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-task-day[aria-pressed=true]{background:var(--bt-user);border-color:var(--bt-user);color:var(--bt-user-ink)} --.bt-task-foot{justify-content:flex-end;gap:8px} --.bt-task-foot .bt-send{height:28px;font-size:12px} --.bt-task-off{align-items:center;gap:8px;padding:28px 20px;text-align:center} --.bt-task-off>*+*{border-top:0} --.bt-task-off-mark{width:44px;height:44px;border-radius:50%;background:var(--bt-hover);display:flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} --.bt-task-off-mark svg{width:20px;height:20px} --.bt-task-off-title{font-size:14px;line-height:20px;font-weight:600} --.bt-task-off .bt-set-hint{max-width:380px} --.bt-task-list{display:flex;flex-direction:column;gap:2px} --.bt-task-line{display:flex;align-items:center;gap:8px;width:100%;min-width:0;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:left;cursor:pointer;transition:background-color .12s ease} --.bt-task-line:hover{background:var(--bt-hover)} --.bt-task-line svg{flex:none;color:var(--bt-ink-3)} --.bt-task-line-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-task-line-when{flex:none;font-size:12px;color:var(--bt-ink-3)} --@media (prefers-reduced-motion:reduce){.bt-task-form{animation:none}} --`; - --// src/client/capability-settings.js --var import_react25 = require("react"); --var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); --var parseMap = (text2, label) => { -- let value; -- try { -- value = JSON.parse(text2 || "{}"); -- } catch { -- throw new Error(t("{label} must be valid JSON.", { label })); -- } -- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(t("{label} must be a JSON object.", { label })); -- return value; -+// 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; - }; --var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); --function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { -- return (0, import_react25.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t(label))), -- (0, import_react25.createElement)(multiline ? "textarea" : "input", { -- className: multiline ? "bt-input bt-input-area" : "bt-input", -- value, -- disabled, -- maxLength: label === "Name" ? 32 : void 0, -- placeholder, -- spellCheck: false, -- onChange: (event) => onChange(event.target.value) -- }) -- ); -+function mentionRows(roster, sessionId, query = "") { -+ const here = conversationOf(roster, sessionId); -+ if (here === null) return null; -+ const face = isFaceRoom(here.room); -+ const live = (here.room?.participants ?? []).filter((p) => p.leftSeq === void 0); -+ const bots = here.room ? [ -+ ...here.room.members.map((id) => roster.byId[id]).filter(Boolean).map((bot) => face ? { ...bot, label: faceLabel(myOwnerName(roster, live, bot), bot.name) } : bot), -+ ...live.filter((p) => p.kind === "human").map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role })), -+ // A remote seat already listed under its handle (a duplicate participant record) is offered once. -+ ...uniqueBy(live.filter((p) => p.remote), (p) => p.handle || p.id).map((p) => ({ -+ id: p.handle || p.id, -+ // A face room inserts the disambiguated title. An ordinary group inserts the name the -+ // room routes on (#1141): never "@@handle.home (owner)" and never "(Friend)". -+ name: face ? remoteBotTitle(p) : remoteCallName(p), -+ kind: "bot", -+ remote: p, -+ handle: p.handle, -+ label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) -+ })) -+ ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); -+ const needle = query.trim().toLowerCase(); -+ const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); -+ const everyone = t("Everyone"); -+ if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -+ rows.push({ room: here.room, count: bots.filter((bot) => bot.kind !== "human").length, text: `@${everyone} ` }); -+ } -+ 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)))]; -+ const remoteHandles = new Set(remoteMentionRows(roster, "").map((row) => normHandle(row.remote.handle))); -+ return [ -+ ...rows.filter((row) => !(row.bot && remoteHandles.has(normHandle(remoteRecord(row.bot)?.handle)))), -+ ...remoteMentionRows(roster, query) -+ ]; - } --function CapabilityForm({ draft, setDraft, items, actions, onClose }) { -- const [error, setError] = (0, import_react25.useState)(""); -- const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); -- const submit = async (event) => { -- event.preventDefault(); -- setError(""); -- let payload; -- try { -- payload = { name: draft.name.trim(), kind: draft.kind, description: draft.description }; -- if (draft.kind === "mcp" && draft.transport === "stdio") { -- payload.transport = "stdio"; -- payload.command = draft.command; -- payload.args = draft.argsText.split("\n").filter((line2) => line2 !== ""); -- payload.env = parseMap(draft.envText, t("Environment")); -- } else if (draft.kind === "mcp") { -- payload.transport = "streamable-http"; -- payload.url = draft.url; -- payload.headers = parseMap(draft.headersText, t("Headers")); -- } else if (draft.kind === "skill") payload.content = draft.content; -- else payload.items = draft.items; -- } catch (failure) { -- setError(failure?.message ?? String(failure)); -- return; -- } -- try { -- await actions.saveCapability(payload); -- onClose(); -- } catch (failure) { -- setError(failure?.message ?? String(failure)); -- } -- }; -- const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); -- return (0, import_react25.createElement)( -- "form", -- { className: "bt-connector-form", onSubmit: submit }, -- (0, import_react25.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), -- (0, import_react25.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Type"))), -- (0, import_react25.createElement)( -- "select", -- { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, -- ["mcp", "skill", "pack"].map((kind) => (0, import_react25.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) -- ) -- ), -- (0, import_react25.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), -- draft.kind === "mcp" ? (0, import_react25.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), -- (0, import_react25.createElement)( -- "select", -- { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, -- (0, import_react25.createElement)("option", { value: "stdio" }, t("Local command")), -- (0, import_react25.createElement)("option", { value: "streamable-http" }, t("HTTP server")) -- ) -- ) : null, -- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react25.createElement)( -- "div", -- null, -- (0, import_react25.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), -- (0, import_react25.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), -- (0, import_react25.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -- ) : null, -- draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react25.createElement)( -- "div", -- null, -- (0, import_react25.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), -- (0, import_react25.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -- ) : null, -- draft.kind === "skill" ? (0, import_react25.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, -- draft.kind === "pack" ? (0, import_react25.createElement)( -- "div", -- { className: "bt-set-card" }, -- available.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react25.createElement)( -- "label", -- { key: item.name, className: "bt-set-row bt-bs-row" }, -- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, item.name)), -- (0, import_react25.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) -- )) -- ) : null, -- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, -- (0, import_react25.createElement)( -- "div", -- { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), -- (0, import_react25.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) -- ) -- ); -+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 }); -+var normHandle = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); -+var remoteRecord = (bot) => bot?.remote ? bot.remote === true ? bot : bot.remote : null; -+var myOwnerName = (roster, live, bot) => { -+ const seat = live.find((p) => !p.remote && p.kind !== "human" && (p.id === bot.id || bot.handle && p.handle === bot.handle)); -+ return seatOwnerName(seat?.ownerName, seat?.handle) || cleanPeerName(roster.self?.displayName, ""); -+}; -+var uniqueBy = (list, key) => { -+ const seen = /* @__PURE__ */ new Set(); -+ return list.filter((item) => { -+ const k = key(item); -+ if (!k || seen.has(k)) return !k; -+ seen.add(k); -+ return true; -+ }); -+}; -+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 CapabilityRow({ item, onEdit, onDelete, onApprove, confirming }) { -- const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); -- const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; -- return (0, import_react25.createElement)( -- "div", -- { className: "bt-connector" }, -- (0, import_react25.createElement)( -- "div", -- { className: "bt-connector-head" }, -- (0, import_react25.createElement)( -- "span", -- { className: "bt-connector-text" }, -- (0, import_react25.createElement)("span", { className: "bt-connector-name" }, item.name), -- (0, import_react25.createElement)("span", { className: "bt-connector-status" }, secondary) -- ), -- (0, import_react25.createElement)( -- "span", -- { className: "bt-connector-actions" }, -- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) -- ) -- ) -- ); -+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 CapabilitiesPage({ roster, actions, run }) { -- const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name)); -- const [draft, setDraft] = (0, import_react25.useState)(null); -- const [confirming, setConfirming] = (0, import_react25.useState)(""); -- (0, import_react25.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(""), 4e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const startNew = () => setDraft(blank()); -- const edit = (item) => setDraft({ -- ...blank(), -- ...item, -- originalName: item.name, -- argsText: (item.args ?? []).join("\n"), -- envText: JSON.stringify(item.env ?? {}, null, 2), -- headersText: JSON.stringify(item.headers ?? {}, null, 2), -- items: [...item.items ?? []] -- }); -- const remove = (item) => { -- if (confirming !== item.name) { -- setConfirming(item.name); -- return; -- } -- setConfirming(""); -- void run(actions.deleteCapability(item.name)); -+function atSources({ roster, sessions, launched, iconFor }) { -+ const bots = { -+ trigger: "@", -+ name: BOTS_SOURCE, -+ order: -20, -+ showGroupTitle: false, -+ async candidates(session, { query }) { -+ const snapshot = roster.getSnapshot(); -+ const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); -+ const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ -+ // A friend's Bot by the one name the row inserts, "Chief (Bob)" (#967); its handle is the -+ // second line (#384). A seat carries its own name; a friend's chat is named as its sidebar -+ // row and header (#498). -+ name: row.bot?.remote && !face ? row.bot.kind === "bot" ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? row.text.replace(/^@/, "").trimEnd() : t("Everyone"), -+ ...row.bot?.role && !face ? { description: row.bot.role } : {}, -+ ...row.remote ? { description: row.remote.handle } : {}, -+ ...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 -+ })); -+ if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; -+ const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ -+ name: row.name, -+ ...row.description ? { description: row.description } : {}, -+ icon: row.icon ?? "session", -+ section: row.section ?? "FlyBot", -+ value: row.value -+ })); -+ return [...team, ...extra]; -+ }, -+ onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 - }; -- return (0, import_react25.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react25.createElement)("h3", null, t("Shared capabilities")), -- (0, import_react25.createElement)( -- "div", -- { className: "bt-actions" }, -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) -- ), -- draft ? (0, import_react25.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, -- (0, import_react25.createElement)( -- "div", -- { className: "bt-set-card bt-connectors-card" }, -- items.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react25.createElement)(CapabilityRow, { -- key: item.name, -- item, -- confirming: confirming === item.name, -- onEdit: () => edit(item), -- onDelete: () => remove(item), -- onApprove: () => void run(actions.approveCapability(item.name)) -- })) -- ) -- ); --} --function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { -- const items = [...roster.capabilities ?? []].sort((a, b) => Number(b.kind === "pack") - Number(a.kind === "pack") || a.name.localeCompare(b.name)); -- const attached = new Set(bot.capabilities ?? []); -- const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; -- if (items.length === 0) return null; -- return (0, import_react25.createElement)( -- Card2, -- { title: t("Capabilities") }, -- items.map((item) => (0, import_react25.createElement)(SwitchRow2, { -- key: item.name, -- label: item.name, -- hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -- on: attached.has(item.name), -- onChange: (enabled) => void run(actions.setBotCapability(bot.id, item.name, enabled)) -- })) -- ); -+ 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]; - } - --// src/client/connector-bots.js --var import_react26 = require("react"); --function ownBots(roster) { -- const me = roster?.self?.memberId; -- return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); --} --var connectedConnectors = (roster) => (roster?.connectors ?? []).filter((connector) => connector.state !== "off" && connector.kind !== "sender" && !(connector.kind === "channel" && connector.mode !== "app")); --var isOn = (bot, id) => bot.connectors?.[id] !== false; --function ownHint(bot, id) { -- return isOn(bot, id) ? t("On. This Bot can use it on every turn.") : t("Off. This Bot will not use it."); --} --function usageText(roster, id) { -- const bots = ownBots(roster); -- const own = bots.filter((bot) => isOn(bot, id)).length; -- return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); -+// src/client/face-at-filter.js -+var REFERENCE_SOURCE = "reference"; -+var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; -+var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; -+var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; -+function planFaceMenu(state, previous = null) { -+ const hidden = []; -+ const ready = []; -+ let shown = 0; -+ let loading = false; -+ for (const group of state?.groups ?? []) { -+ const items = group.items ?? []; -+ if (group.status !== "ready" && items.length === 0) loading = true; -+ items.forEach((item, index) => { -+ const hide = isSessionRow(group, item); -+ if (hide) hidden.push({ source: group.source, index, title: item.section !== void 0 && item.section !== items[index - 1]?.section }); -+ else shown += 1; -+ if (group.status === "ready") ready.push({ source: group.source, index, hide }); -+ }); -+ } -+ const open = state?.open === true; -+ const blank3 = open && hidden.length > 0 && shown === 0 && !loading; -+ const dismiss = blank3 && (state.groups ?? []).every((group) => group.status === "ready"); -+ return { hidden, blank: blank3, dismiss, target: dismiss ? null : steer(ready, state?.highlight ?? null, previous) }; - } --function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { -- return (0, import_react26.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); -+function steer(ready, highlight, previous) { -+ const at = ready.findIndex((row) => same(row, highlight)); -+ if (at < 0 || !ready[at].hide) return null; -+ const from = ready.findIndex((row) => same(row, previous)); -+ const n = ready.length; -+ const dir = from >= 0 && from !== at && (from - at + n) % n === 1 ? -1 : 1; -+ for (let step = 1; step < n; step += 1) { -+ const row = ready[((at + dir * step) % n + n) % n]; -+ if (!row.hide) return { source: row.source, index: row.index }; -+ } -+ return null; - } --function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { -- const bots = ownBots(roster); -- return (0, import_react26.createElement)( -- "div", -- { className: "bt-connector-bots" }, -- bots.length === 0 ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react26.createElement)( -- "div", -- { key: bot.id, className: "bt-connector-bot" }, -- (0, import_react26.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) -- )), -- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) -- ); -+function faceMenuCss({ hidden, blank: blank3 }) { -+ if (hidden.length === 0) return ""; -+ const rows = hidden.map((row) => `[data-trigger-menu] [id="${optionId(row.source, row.index)}"]`); -+ const titles = hidden.filter((row) => row.title).map((row) => `[data-trigger-menu] [role="presentation"]:has(+ [id="${optionId(row.source, row.index)}"])`); -+ const css = [`${[...rows, ...titles].join(",")}{display:none!important}`]; -+ if (blank3) css.push("[data-trigger-menu]{display:none!important}"); -+ return css.join("\n"); - } --function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { -- const connected = connectedConnectors(roster); -- if (connected.length === 0) return null; -- return (0, import_react26.createElement)( -- Card2, -- { title: t("Connectors") }, -- connected.map((connector) => (0, import_react26.createElement)( -- "div", -- { key: connector.id, className: "bt-connector-bot" }, -- (0, import_react26.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) -- )) -- ); -+function watchFaceMenu(controller, doc = globalThis.document) { -+ if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { -+ }; -+ const style = doc.createElement("style"); -+ style.setAttribute("data-bt-face-at", ""); -+ doc.head.appendChild(style); -+ let previous = null; -+ let stopped = false; -+ const later = typeof queueMicrotask === "function" ? queueMicrotask : (fn) => Promise.resolve().then(fn); -+ const apply2 = () => { -+ if (stopped) return; -+ const state = controller.menu.getSnapshot(); -+ const plan = planFaceMenu(state, previous); -+ const css = faceMenuCss(plan); -+ if (style.textContent !== css) style.textContent = css; -+ if (plan.dismiss) later(() => { -+ if (!stopped && planFaceMenu(controller.menu.getSnapshot(), previous).dismiss) controller.dismiss(); -+ }); -+ else if (plan.target) later(() => { -+ if (!stopped) controller.hover(plan.target.source, plan.target.index); -+ }); -+ else previous = state.open ? state.highlight : null; -+ }; -+ const unsubscribe = controller.menu.subscribe(apply2); -+ apply2(); -+ return () => { -+ stopped = true; -+ unsubscribe?.(); -+ style.remove(); -+ }; - } - --// src/client/memory-panel.js --var import_react27 = require("react"); --var POLL_MS5 = 1500; --var MENU_EXIT_MS2 = 120; --var counts = /* @__PURE__ */ new Map(); --var countListeners = /* @__PURE__ */ new Set(); --function noteCount(botId, count) { -- if (counts.get(botId) === count) return; -- counts.set(botId, count); -- for (const listener of countListeners) listener(); --} --function useMemoryCount(botId, actions) { -- const [count, setCount] = (0, import_react27.useState)(counts.get(botId) ?? null); -- (0, import_react27.useEffect)(() => { -- const listener = () => setCount(counts.get(botId) ?? null); -- countListeners.add(listener); -- listener(); -- let stop = false; -- void actions.memoryView?.(botId).then((view) => { -- if (!stop) noteCount(botId, view.entries.length); -- }).catch(() => { -- }); -- return () => { -- stop = true; -- countListeners.delete(listener); -- }; -- }, [botId]); -- return count; -+// 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 MemoryRow({ bot, actions }) { -- const count = useMemoryCount(bot.id, actions); -- const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -- return (0, import_react27.createElement)( -- "div", -- null, -- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Memory")), -- (0, import_react27.createElement)( -- "button", -- { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -- (0, import_react27.createElement)(MemoryIcon), -- (0, import_react27.createElement)("span", { className: "bt-mem-row-copy" }, label), -- (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon)) -- ) -- ); --} --function ago(at, now) { -- const seconds = Math.max(0, Math.round((now - at) / 1e3)); -- if (seconds < 60) return null; -- const format = new Intl.RelativeTimeFormat(dateLocale(), { numeric: "auto" }); -- const minutes = Math.round(seconds / 60); -- if (minutes < 60) return format.format(-minutes, "minute"); -- const hours = Math.round(minutes / 60); -- if (hours < 24) return format.format(-hours, "hour"); -- return format.format(-Math.round(hours / 24), "day"); -+function waveBytes(samples, rate = 16e3) { -+ const bytes = new Uint8Array(44 + samples.length * 2); -+ const view = new DataView(bytes.buffer); -+ const ascii = (at, text2) => { -+ for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); -+ }; -+ ascii(0, "RIFF"); -+ view.setUint32(4, 36 + samples.length * 2, true); -+ 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 Skeleton({ label }) { -- return (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-skel", "aria-busy": true }, -- [0, 1].map((block) => (0, import_react27.createElement)( -- "div", -- { key: block, className: "bt-mem-skel-block" }, -- (0, import_react27.createElement)("span", { className: "bt-mem-skel-head" }), -- (0, import_react27.createElement)("span", null), -- (0, import_react27.createElement)("span", null), -- (0, import_react27.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) -- )), -- label ? (0, import_react27.createElement)("div", { className: "bt-mem-skel-label" }, label) : null -- ); -+function base64Of(bytes) { -+ let text2 = ""; -+ for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); -+ return btoa(text2); - } --function Summary({ bot, view, regenerate }) { -- if (view.entries.length === 0) { -- return (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-empty" }, -- (0, import_react27.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react27.createElement)(MemoryIcon)), -- (0, import_react27.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), -- (0, import_react27.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) -- ); -+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 failed = view.error && !view.summarizing ? (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-failed", role: "alert" }, -- (0, import_react27.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), -- (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) -- ) : null; -- if (!view.summary) return (0, import_react27.createElement)(import_react27.Fragment, null, failed, failed ? null : (0, import_react27.createElement)(Skeleton, { label: t("Writing the summary…") })); -- return (0, import_react27.createElement)( -- import_react27.Fragment, -- null, -- failed, -- (0, import_react27.createElement)( -- "div", -- { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, -- summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react27.createElement)("h3", { key: index }, block.text) : (0, import_react27.createElement)("p", { key: index }, block.text)) -- ) -- ); -+ 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); -+ }; -+ 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; -+ } -+ 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(); -+ } -+ } -+ }; - } --var groupEntries = (entries2) => entries2.reduce((groups, entry) => { -- const key = `${entry.scope}:${entry.topic ?? ""}`; -- const group = groups.find((item) => item.key === key); -- if (group) group.entries.push(entry); -- else groups.push({ key, scope: entry.scope, topic: entry.topic, entries: [entry] }); -- return groups; --}, []); --function EntryRow({ entry, remove }) { -- const [confirming, setConfirming] = (0, import_react27.useState)(false); -- const [busy, setBusy] = (0, import_react27.useState)(false); -- (0, import_react27.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 3e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const by = entry.source === "memory panel" ? t("by you") : entry.by ? t("by {name}", { name: entry.by }) : null; -- const meta = [entry.added, by].filter(Boolean).join(" · "); -- const click = async () => { -- if (!confirming) { -- setConfirming(true); -- return; -+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"; - } -- setBusy(true); -- if (!await remove(entry)) setBusy(false); -+ if (!spokeAt) return "waiting"; -+ if (level < quiet) quietSince ||= now; -+ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; - }; -- return (0, import_react27.createElement)( -- "li", -- { className: "bt-mem-entry", "data-busy": busy || void 0 }, -- (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-entry-copy" }, -- (0, import_react27.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), -- meta ? (0, import_react27.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null -- ), -- (0, import_react27.createElement)("button", { -- type: "button", -- className: "bt-mem-remove", -- "data-confirm": confirming || void 0, -- disabled: busy, -- "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), -- title: confirming ? void 0 : t("Remove from memory"), -- onClick: click -- }, confirming ? t("Remove") : (0, import_react27.createElement)(TrashIcon)) -- ); - } --function Entries({ bot, view, remove }) { -- const groups = groupEntries(view.entries); -- const sections = [ -- { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, -- { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } -+ -+// src/client/model-menu.js -+var import_react22 = 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 servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); -+var effortCache = /* @__PURE__ */ new Map(); -+function useEfforts(served, ref, actions) { -+ const [fetched, setFetched] = (0, import_react22.useState)(void 0); -+ (0, import_react22.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(() => { -+ }); -+ return () => { -+ live = false; -+ }; -+ }, [ref, served?.reasoning !== void 0, actions]); -+ return served?.reasoning ?? fetched; -+} -+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 })) - ]; -- return (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-entries" }, -- sections.map((section) => (0, import_react27.createElement)( -- "section", -- { key: section.scope, className: "bt-mem-section" }, -- (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-section-head" }, -- (0, import_react27.createElement)("h3", null, section.title), -- section.hint ? (0, import_react27.createElement)("span", null, section.hint) : null, -- (0, import_react27.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) -- ), -- groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react27.createElement)( -- import_react27.Fragment, -- { key: group.key }, -- group.topic ? (0, import_react27.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, -- (0, import_react27.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react27.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) -- )) : (0, import_react27.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) -+} -+function EffortRows({ reasoning, current, onPick }) { -+ return (0, import_react22.createElement)( -+ import_react22.Fragment, -+ null, -+ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -+ effortRows(reasoning).map((row) => (0, import_react22.createElement)( -+ "button", -+ { -+ key: row.effort ?? "default", -+ type: "button", -+ role: "menuitemradio", -+ "aria-checked": row.effort === current, -+ onClick: () => onPick(row.effort) -+ }, -+ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, row.label), -+ (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react22.createElement)(CheckIcon) : null) - )) - ); - } --function changeLine(change) { -- const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; -- if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); -- if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); -- if (change.op === "edit") return t("Updated: {text}", { text: text2 }); -- return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 }); --} --function Reply({ answer, dismiss }) { -- return (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-reply", role: "status" }, -- (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-reply-head" }, -- (0, import_react27.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), -- answer.pending ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react27.createElement)(CloseIcon)) -- ), -- answer.pending ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react27.createElement)( -- import_react27.Fragment, -- null, -- answer.reply ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, -- answer.changes.length > 0 ? (0, import_react27.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react27.createElement)( -- "li", -- { key: index, "data-ok": change.ok || void 0 }, -- change.ok ? (0, import_react27.createElement)(CheckIcon) : (0, import_react27.createElement)(CloseIcon), -- (0, import_react27.createElement)("span", null, changeLine(change)) -- ))) : null -- ) -- ); --} --function useMemoryView(botId, actions) { -- const [view, setView] = (0, import_react27.useState)(null); -- const [error, setError] = (0, import_react27.useState)(""); -- const timer = (0, import_react27.useRef)(0); -- const latest = (0, import_react27.useRef)(0); -- const alive = (0, import_react27.useRef)(true); -- const show = (0, import_react27.useCallback)((next) => { -- setView(next); -- setError(""); -- noteCount(botId, next.entries.length); -- }, [botId]); -- const load = (0, import_react27.useCallback)(async (flags = {}) => { -- clearTimeout(timer.current); -- const ticket = ++latest.current; -- try { -- const next = await actions.memoryView(botId, flags); -- if (!alive.current || ticket !== latest.current) return; -- show(next); -- if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5); -- } catch (failure) { -- if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure)); -- } -- }, [botId]); -- (0, import_react27.useEffect)(() => { -- alive.current = true; -- void load({ summarize: true }); -- return () => { -- alive.current = false; -- clearTimeout(timer.current); -- }; -- }, [botId]); -- return { view, error, load, show }; --} --function useNow3(every) { -- const [now, setNow] = (0, import_react27.useState)(Date.now()); -- (0, import_react27.useEffect)(() => { -- const id = setInterval(() => setNow(Date.now()), every); -- return () => clearInterval(id); -- }, [every]); -- return now; -+function ModelRows({ roster, current, onPick }) { -+ return groupModels(roster.models).map(([provider, list]) => (0, import_react22.createElement)( -+ import_react22.Fragment, -+ { key: list[0].provider ?? provider }, -+ (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -+ list.map((model) => (0, import_react22.createElement)( -+ "button", -+ { -+ key: model.ref, -+ type: "button", -+ role: "menuitemradio", -+ "aria-checked": model.ref === current, -+ title: model.ref, -+ onClick: () => onPick(model.ref) -+ }, -+ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, model.name), -+ model.ref === roster.defaultModel ? (0, import_react22.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -+ (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react22.createElement)(CheckIcon) : null) -+ )) -+ )); - } --function MemoryDialog({ bot, actions, leaving = false }) { -- const { view, error, load, show } = useMemoryView(bot.id, actions); -- const [page, setPage] = (0, import_react27.useState)("summary"); -- const [menu, setMenu] = (0, import_react27.useState)(false); -- const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); -- const [text2, setText] = (0, import_react27.useState)(""); -- const [answer, setAnswer] = (0, import_react27.useState)(null); -- const menuRef = (0, import_react27.useRef)(null); -- const dotsRef = (0, import_react27.useRef)(null); -- const inputRef = (0, import_react27.useRef)(null); -- const now = useNow3(3e4); -- const close = (0, import_react27.useCallback)(() => actions.closeMemory(), []); -- useEscape(close); -- (0, import_react27.useEffect)(() => { -- if (!menu) return void 0; -+var Caret = () => (0, import_react22.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_react22.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); -+function useAnchoredMenu() { -+ const [place, setPlace] = (0, import_react22.useState)(null); -+ const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); -+ const button = (0, import_react22.useRef)(null); -+ const menu = (0, import_react22.useRef)(null); -+ const close = () => setPlace(null); -+ (0, import_react22.useEffect)(() => { -+ if (place === null) return void 0; - const onDown = (event) => { -- if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -- setMenu(false); -+ if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -+ close(); - }; - const onKey = (event) => { - if (event.key === "Escape") { - event.stopPropagation(); -- setMenu(false); -- dotsRef.current?.focus(); -+ 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); - }; -- }, [menu]); -- const regenerate = () => { -- setMenu(false); -- setPage("summary"); -- void load({ regenerate: 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" }); - }; -- const openEntries = () => { -- setMenu(false); -- setPage("entries"); -+ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; -+} -+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; -+ } -+ void actions?.refreshModels?.(); -+ open(); - }; -- const backToSummary = () => { -- setPage("summary"); -- void load({ summarize: true }); -+ const pick2 = (ref) => { -+ close(); -+ if (onPick) onPick(ref); -+ else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); - }; -- const remove = async (entry) => { -- try { -- show(await actions.memoryForget(bot.id, entry)); -- return true; -- } catch (failure) { -- setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) }); -- return false; -- } -+ const pickEffort = (effort) => { -+ close(); -+ if (onPickEffort) onPickEffort(effort); -+ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); - }; -- const ask = async (event) => { -- event.preventDefault(); -- const asked = text2.trim(); -- if (asked === "" || answer?.pending) return; -- setText(""); -- setAnswer({ asked, pending: true }); -- try { -- const result = await actions.memoryAsk(bot.id, asked); -- setAnswer({ asked, reply: result.reply, changes: result.changes }); -- show(result.view); -- if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" }); -- } catch (failure) { -- setAnswer({ asked, error: failure?.message ?? String(failure) }); -- } -- inputRef.current?.focus(); -+ const models = roster.models ?? []; -+ return (0, import_react22.createElement)( -+ import_react22.Fragment, -+ null, -+ (0, import_react22.createElement)( -+ "button", -+ { -+ ref: button, -+ type: "button", -+ className: "bt-model-trigger", -+ "data-wide": wide || void 0, -+ "aria-haspopup": "menu", -+ "aria-expanded": isOpen, -+ "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), -+ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, -+ onClick: toggle -+ }, -+ (0, import_react22.createElement)( -+ "span", -+ { className: "bt-model-copy" }, -+ (0, import_react22.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -+ shown ? (0, import_react22.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_react22.createElement)(Caret) -+ ), -+ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react22.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_react22.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react22.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -+ effortRows(reasoning).length > 0 ? (0, import_react22.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -+ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -+ close(); -+ openHarnessModels(); -+ } }, (0, import_react22.createElement)(SlidersIcon), (0, import_react22.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_react22.useState)(""); -+ const [error, setError] = (0, import_react22.useState)(""); -+ const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; -+ (0, import_react22.useEffect)(() => { -+ 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_react22.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(""); -+ void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { -+ setError(failure?.message ?? String(failure)); -+ void actions.refreshModels?.(); -+ }).finally(() => setBusy("")); - }; -- const count = view?.entries.length ?? 0; -- const when3 = view?.summary ? ago(view.summary.at, now) : null; -- const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; -- const busy = answer?.pending === true; -- return (0, import_react27.createElement)( -+ 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_react22.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react22.createElement)(KeyHelpLink, { key: "key-help" })] : [ -+ ...groupModels(models).map(([provider, list]) => (0, import_react22.createElement)( -+ "div", -+ { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, -+ (0, import_react22.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -+ (0, import_react22.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react22.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_react22.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -+ )), -+ (0, import_react22.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react22.createElement)(SlidersIcon), t("Manage models")) -+ ]; -+ return (0, import_react22.createElement)( - "div", -- { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, -- (0, import_react27.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -- (0, import_react27.createElement)( -+ { className: "bt-model-dock" }, -+ (0, import_react22.createElement)( - "div", -- { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, -- (0, import_react27.createElement)( -+ { className: "bt-astack", "data-lead": "" }, -+ (0, import_react22.createElement)( - "div", -- { className: "bt-mem-head" }, -- page === "entries" ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react27.createElement)(ChevronLeftIcon)) : null, -- (0, import_react27.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -- (0, import_react27.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -- (0, import_react27.createElement)("span", { className: "bt-mem-head-gap" }), -- (0, import_react27.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react27.createElement)(DotsIcon)), -- (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react27.createElement)(CloseIcon)), -- menuShown ? (0, import_react27.createElement)( -+ { className: "bt-msg" }, -+ (0, import_react22.createElement)( - "div", -- { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, -- (0, import_react27.createElement)( -- "button", -- { type: "button", role: "menuitem", onClick: openEntries }, -- (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -- (0, import_react27.createElement)("span", { className: "bt-menu-hint" }, String(count)) -- ), -- (0, import_react27.createElement)( -- "button", -- { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, -- (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) -- ) -- ) : null -- ), -- (0, import_react27.createElement)( -- "div", -- { className: "bt-mem-body" }, -- error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -- view === null ? error ? null : (0, import_react27.createElement)(Skeleton) : page === "entries" ? (0, import_react27.createElement)(Entries, { bot, view, remove }) : (0, import_react27.createElement)(Summary, { bot, view, regenerate }) -- ), -- (0, import_react27.createElement)( -+ { className: "bt-msg-line" }, -+ (0, import_react22.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react22.createElement)("div", { className: "bt-bubble" }, text2) -+ ) -+ ) -+ ), -+ (0, import_react22.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -+ error ? (0, import_react22.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -+ ); -+} -+function RemoteGoneDock({ bot }) { -+ return (0, import_react22.createElement)( -+ "div", -+ { className: "bt-model-dock", "data-flybot": "remote-gone" }, -+ (0, import_react22.createElement)( -+ "div", -+ { className: "bt-astack", "data-lead": "" }, -+ (0, import_react22.createElement)( - "div", -- { className: "bt-mem-foot" }, -- answer ? (0, import_react27.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -- (0, import_react27.createElement)( -- "form", -- { className: "bt-mem-ask", onSubmit: ask }, -- (0, import_react27.createElement)("input", { -- ref: inputRef, -- className: "bt-mem-input", -- value: text2, -- maxLength: 2e3, -- disabled: view === null, -- placeholder: t("Ask or update"), -- "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), -- onChange: (event) => setText(event.target.value) -- }), -- (0, import_react27.createElement)( -- "button", -- { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") }, -- busy ? (0, import_react27.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react27.createElement)(ArrowUpIcon) -- ) -+ { className: "bt-msg" }, -+ (0, import_react22.createElement)( -+ "div", -+ { className: "bt-msg-line" }, -+ (0, import_react22.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react22.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 })) - ) - ) - ) - ); - } -- --// src/client/standing-approvals.js --var import_react28 = require("react"); --var standingOf = (target, roster) => { -- const me = roster?.self?.memberId; -- return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); --}; --function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card2 }) { -- const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; -- if (grants.length === 0) return null; -- const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); -- return (0, import_react28.createElement)( -- Card2, -- { title: t("Always allowed") }, -- grants.map((grant) => (0, import_react28.createElement)( -- "div", -- { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, -- (0, import_react28.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react28.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), -- (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) -- ), -- (0, import_react28.createElement)("button", { -- type: "button", -- className: "bt-soft bt-soft-sm", -- "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), -- onClick: () => run(actions.removeStandingApproval(scope, target.id, grant.kind)) -- }, t("Remove")) -- )) -- ); --} -- --// src/client/bot-settings.js --var import_react29 = require("react"); --var capitalize = (word) => word[0].toUpperCase() + word.slice(1); --async function avatarImage(file) { -- if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -- if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB")); -- const bitmap = await createImageBitmap(file); -- const side = 160; -- const canvas = document.createElement("canvas"); -- canvas.width = side; -- canvas.height = side; -- const context = canvas.getContext("2d"); -- context.imageSmoothingQuality = "high"; -- const crop = Math.min(bitmap.width, bitmap.height); -- context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side); -- bitmap.close?.(); -- for (const quality of [0.9, 0.75, 0.6]) { -- const url = canvas.toDataURL("image/webp", quality); -- if (url.startsWith("data:image/webp") && url.length < 15e4) return url; -- } -- const png = canvas.toDataURL("image/png"); -- if (png.length < 15e4) return png; -- throw new Error(t("That image is too large")); --} --function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { -- const [draft, setDraft] = (0, import_react29.useState)(null); -- const reverting = (0, import_react29.useRef)(false); -- const commit = () => { -- const next = draft?.trim(); -- setDraft(null); -- if (reverting.current) { -- reverting.current = false; -- return; -- } -- if (next === void 0 || next === (value ?? "") || required && next === "") return; -- void onSave(next); -- }; -- return (0, import_react29.createElement)(multiline ? "textarea" : "input", { -- className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", -- value: draft ?? value ?? "", -- placeholder, -- maxLength, -- required, -- "aria-label": label, -- spellCheck: false, -- onChange: (event) => setDraft(event.target.value), -- onBlur: commit, -- onKeyDown: (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- reverting.current = true; -- event.currentTarget.blur(); -- } -- if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur(); -- } -- }); --} --function Row({ label, hint, stack = false, children }) { -- return (0, import_react29.createElement)( -- "label", -- { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- children -- ); --} --function Card({ title, children }) { -- return (0, import_react29.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react29.createElement)("h3", null, title) : null, (0, import_react29.createElement)("div", { className: "bt-set-card" }, children)); --} --function Switch({ on, label, disabled, onChange }) { -- return (0, import_react29.createElement)( -- "button", -- { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, -- (0, import_react29.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) -- ); --} --function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { -- return (0, import_react29.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- (0, import_react29.createElement)(Switch, { on, label, disabled, onChange }) -- ); -+function 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 modelHint(bot, roster) { -- const shown = modelOf(bot, roster); -- if (shown === void 0) return t("Pick the model this Bot runs on."); -- if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one."); -- if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it."); -- return t("Applies from the next message."); -+var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; -+function KeyHelpLink() { -+ return (0, import_react22.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); - } --function ShapeRing({ shape, size }) { -- const maskId = (0, import_react29.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; -- const def = shapeOf(shape); -- const unit = def.size / size; -- const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; -- return (0, import_react29.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); -+function missingModelKey(message) { -+ const text2 = String(message ?? ""); -+ if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; -+ return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; - } --function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { -- const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; -- if (!shapes.includes(look.shape)) shapes.push(look.shape); -- const rows = []; -- for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); -- return (0, import_react29.createElement)( -- import_react29.Fragment, -- null, -- (0, import_react29.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react29.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react29.createElement)("button", { -- key: id, -- type: "button", -- className: "bt-shape", -- "aria-label": t(shapeOf(id).label), -- title: t(shapeOf(id).label), -- "aria-pressed": !hasPhoto && look.shape === id, -- onClick: () => onShape(id) -- }, (0, import_react29.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react29.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -- (0, import_react29.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react29.createElement)("button", { -- key: color, -- type: "button", -- className: "bt-color", -- "aria-label": t(capitalize(color)), -- title: t(capitalize(color)), -- "aria-pressed": look.color === color, -- onClick: () => onColor(color) -- }, (0, import_react29.createElement)("span", { style: { background: paintCss(color) } })))) -- ); -+function modelKeyChoices(actions, route) { -+ if (/^deepseek/i.test(route)) return setupChoices(actions); -+ return [[t("Manage models"), openHarnessModels]]; - } --function AvatarEditor({ bot, actions, run, onClose }) { -- const look = lookOf(bot); -- const [tab, setTab] = (0, import_react29.useState)(look.image ? "upload" : "bot"); -- const [over, setOver] = (0, import_react29.useState)(false); -- const ref = (0, import_react29.useRef)(null); -- const fileRef = (0, import_react29.useRef)(null); -- const upload = (file) => { -- if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); -+function TurnErrorCell({ node, t: chatT, actions }) { -+ const data = node?.data ?? {}; -+ const code = typeof data.code === "string" && data.code ? data.code : void 0; -+ const raw = String(data.message ?? ""); -+ const shell = (key, fallback) => { -+ const text2 = typeof chatT === "function" ? chatT(key) : void 0; -+ return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; - }; -- (0, import_react29.useEffect)(() => { -- const onDown = (event) => { -- if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; -- onClose(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- onClose(); -- } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [onClose]); -- (0, import_react29.useEffect)(() => { -- if (tab !== "upload") return void 0; -- const onPaste = (event) => { -- const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -- if (file) { -- event.preventDefault(); -- upload(file); -- } -- }; -- window.addEventListener("paste", onPaste); -- return () => window.removeEventListener("paste", onPaste); -- }, [tab, bot]); -- const hasPhoto = Boolean(look.image); -- const glide = useGlide(tab); -- return (0, import_react29.createElement)( -+ const missing = missingModelKey(raw); -+ const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); -+ const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; -+ const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; -+ const deepseek = missing ? /^deepseek/i.test(missing.route) : false; -+ return (0, import_react22.createElement)( - "div", -- { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, -- (0, import_react29.createElement)( -+ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, -+ (0, import_react22.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), -+ (0, import_react22.createElement)( - "div", -- { className: "bt-avedit-top" }, -- (0, import_react29.createElement)( -- "div", -- { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, -- (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react29.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) -- ), -- (0, import_react29.createElement)("button", { -- type: "button", -- className: "bt-avedit-tab bt-avedit-reset", -- title: t("Reset character to default"), -- onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) -- }, t("Reset")) -+ { className: "bt-turn-error-copy" }, -+ (0, import_react22.createElement)("span", { className: "bt-turn-error-title" }, title), -+ (0, import_react22.createElement)("span", { className: "bt-turn-error-message" }, message), -+ code ? (0, import_react22.createElement)("code", { className: "bt-turn-error-code" }, code) : null - ), -- tab === "bot" ? (0, import_react29.createElement)(CharacterGrid, { -- look, -- hasPhoto, -- onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), -- onColor: (color) => run(actions.updateBot(bot.id, { color })) -- }) : null, -- tab === "upload" ? (0, import_react29.createElement)( -+ choices.length > 0 ? (0, import_react22.createElement)( - "div", -- { -- className: "bt-dropzone", -- "data-over": over || void 0, -- onDragOver: (event) => { -- event.preventDefault(); -- setOver(true); -- }, -- onDragLeave: () => setOver(false), -- onDrop: (event) => { -- event.preventDefault(); -- setOver(false); -- upload(event.dataTransfer.files?.[0]); -- } -- }, -- hasPhoto ? (0, import_react29.createElement)("img", { src: look.image, alt: "" }) : null, -- (0, import_react29.createElement)("div", null, t("Drag, drop, or paste an image")), -- (0, import_react29.createElement)( -- "div", -- { className: "bt-actions", style: { justifyContent: "center" } }, -- (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -- hasPhoto ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null -- ), -- (0, import_react29.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -- const file = event.target.files?.[0]; -- event.target.value = ""; -- upload(file); -- } }) -+ { className: "bt-turn-error-actions" }, -+ choices.map(([label, run]) => (0, import_react22.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), -+ deepseek ? (0, import_react22.createElement)(KeyHelpLink, { key: "key-help" }) : null - ) : null - ); - } --function MemoryCard({ bot, actions }) { -- const count = useMemoryCount(bot.id, actions); -- const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -- return (0, import_react29.createElement)( -- Card, -- { title: t("Memory") }, -- (0, import_react29.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label)), -- (0, import_react29.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react29.createElement)(ChevronRightIcon)) -- ) -- ); -+function DeepSeekSignIn({ useUi, actions }) { -+ const open = useUi((state) => state.signIn); -+ const entry = open ? actions.signInEntry?.() : null; -+ if (!entry) return null; -+ return (0, import_react22.createElement)(SignInHost, { key: open, entry, actions }); - } --function BotSettingsForm({ bot, roster, actions, run }) { -- const [editing, setEditing] = (0, import_react29.useState)(false); -- const [confirming, setConfirming] = (0, import_react29.useState)(false); -- const closeEditor = (0, import_react29.useCallback)(() => setEditing(false), []); -- (0, import_react29.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 4e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- if (bot.remote) return null; -- const isMainBot = isMainOf(roster, bot.id); -- const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; -- const isTile = roster.mainBotId === bot.id; -- const save = (patch) => run(actions.updateBot(bot.id, patch)); -- const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id))); -- const remove = () => { -- if (confirming) void run(actions.deleteBot(bot.id)); -- else setConfirming(true); -+function SignInHost({ entry, actions }) { -+ const [ops] = (0, import_react22.useState)(() => entry.inject?.() ?? {}); -+ const close = () => actions.closeSignIn(); -+ const useHook = (source) => (selector) => (0, import_react22.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -+ const [hooks] = (0, import_react22.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -+ return (0, import_react22.createElement)(entry.component, { -+ ...ops, -+ ...hooks, -+ t: actions.accountT(), -+ complete: close, -+ useApiKey: () => { -+ close(); -+ openHarnessOnboarding("deepseek-official"); -+ } -+ }); -+} -+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} -+.bt-turn-error-copy{min-width:0;overflow-wrap:anywhere} -+.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135} -+.bt-turn-error-message{color:var(--bt-ink-2)} -+.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} -+.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px} -+.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px} -+.bt-model-help:hover{color:var(--bt-ink)} -+.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px} -+.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/model-test.js -+var import_react23 = require("react"); -+var FAILURE_COPY = { -+ NO_MODEL: "Pick a model first.", -+ NO_KEY: "This model has no API key yet. Set one up in the model settings.", -+ AUTH: "The API key was rejected. Check it in the model settings.", -+ QUOTA: "The account is out of quota.", -+ RATE_LIMIT: "Too many requests right now. Try again in a moment.", -+ TIMEOUT: "The model did not answer within 30 seconds.", -+ NOT_SERVED: "This model is not available now. Check its provider in the model settings.", -+ MODEL_NOT_FOUND: "The provider does not know this model. Check the model name.", -+ TRANSPORT: "Could not reach the model service. Check the endpoint address and your network.", -+ SERVER: "The model service failed. Try again later.", -+ INVALID_REQUEST: "The model service refused the request. Check the endpoint and the model.", -+ UNKNOWN: "The test failed." -+}; -+function testOutcome(result, modelName) { -+ if (result?.ok === true) { -+ const seconds = typeof result.ms === "number" ? Math.max(0.1, Math.round(result.ms / 100) / 10) : void 0; -+ return { ok: true, text: seconds === void 0 ? t("Works. {model} answered.", { model: modelName }) : t("Works. {model} answered in {seconds} s.", { model: modelName, seconds }) }; -+ } -+ const code = Object.hasOwn(FAILURE_COPY, result?.code) ? result.code : "UNKNOWN"; -+ return { ok: false, code, text: t(FAILURE_COPY[code]) }; -+} -+var testModel = (id) => call("test-model", { id }); -+function ModelTestRow({ bot, modelName, test = testModel }) { -+ const [state, setState] = (0, import_react23.useState)({ busy: false, outcome: void 0 }); -+ const live = (0, import_react23.useRef)(true); -+ (0, import_react23.useEffect)(() => () => { -+ live.current = false; -+ }, []); -+ (0, import_react23.useEffect)(() => { -+ setState({ busy: false, outcome: void 0 }); -+ }, [bot.id, bot.model]); -+ const start = () => { -+ if (state.busy) return; -+ setState({ busy: true, outcome: void 0 }); -+ Promise.resolve().then(() => test(bot.id)).then( -+ (result) => testOutcome(result, modelName ?? result?.ref ?? ""), -+ (error) => ({ ok: false, text: String(error?.message ?? t("The test failed.")) }) -+ ).then((outcome2) => { -+ if (live.current) setState({ busy: false, outcome: outcome2 }); -+ }); - }; -- (0, import_react29.useEffect)(() => { -- void actions.refreshModels?.(); -- }, [bot.id]); -- return (0, import_react29.createElement)( -+ const { busy, outcome } = state; -+ return (0, import_react23.createElement)( - "div", -- { className: "bt-bs" }, -- (0, import_react29.createElement)( -- "div", -- { className: "bt-bs-hero" }, -- (0, import_react29.createElement)( -- "div", -- { className: "bt-avatar-anchor" }, -- (0, import_react29.createElement)( -- "button", -- { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -- (0, import_react29.createElement)(BotMark, { bot, size: 64, live: true }), -- (0, import_react29.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react29.createElement)(PencilIcon)) -- ), -- editing ? (0, import_react29.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null -- ), -- (0, import_react29.createElement)( -- "div", -- { className: "bt-bs-who" }, -- (0, import_react29.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) -- ) -- ), -- (0, import_react29.createElement)( -- Card, -- { title: t("Profile") }, -- (0, import_react29.createElement)(Row, { label: t("Name") }, (0, import_react29.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react29.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react29.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) -- ), -- (0, import_react29.createElement)( -- Card, -- { title: t("Model") }, -- (0, import_react29.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -- (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run }) -- ), -- bot.model ? (0, import_react29.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null -- ), -- (0, import_react29.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react29.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react29.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) -- ) -- ), -- (0, import_react29.createElement)( -- Card, -- { title: t("Instructions") }, -- (0, import_react29.createElement)( -- Row, -- { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -- (0, import_react29.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) -+ { className: "bt-set-row bt-bs-row bt-model-test" }, -+ (0, import_react23.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), -+ (0, import_react23.createElement)( -+ "span", -+ { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, -+ busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, -+ outcome?.code ? (0, import_react23.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null - ) - ), -- // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). -- (0, import_react29.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react29.createElement)(Card, { title: t("Sharing") }, children) }), -- (0, import_react29.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -- (0, import_react29.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), -- roster.memory === false ? null : (0, import_react29.createElement)(MemoryCard, { bot, actions }), -- (0, import_react29.createElement)(BotSchedules, { bot, roster, actions }), -- (0, import_react29.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), -- (0, import_react29.createElement)( -- Card, -- { title: t("Main Bot") }, -- onlyMain ? (0, import_react29.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react29.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -- ), -- (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -- ) : (0, import_react29.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) -- ), -- // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react29.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react29.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -- ), -- (0, import_react29.createElement)( -- Card, -- { title: t("More") }, -- (0, import_react29.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -- (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) -- ), -- isMainBot ? null : (0, import_react29.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react29.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -- ), -- (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) -- ) -- ) -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) - ); - } --var BOT_SETTINGS_CSS = ` --.bt-bs{display:flex;flex-direction:column;gap:18px} --.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} --.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} --.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-bs .bt-avedit{left:0;transform:none} --.bt-bs-row{min-height:48px} --label.bt-bs-row{cursor:text} --.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} --/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to -- break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test -- connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under -- a label is prose, so it wraps and is never cut: the row grows instead. */ --.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} --.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} --.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} --.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} --${MODEL_TEST_CSS} --.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} --.bt-bs-input:hover{background:var(--bt-hover)} --.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} --.bt-bs-input::placeholder{color:var(--bt-ink-3)} --.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} --.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} --.bt-bs-area:hover{background:var(--bt-active)} --.bt-bs-danger .bt-bs-label{color:#c21d2e} --.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} --.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} --.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} --.bt-switch[aria-checked=true]{background:var(--bt-accent)} --.bt-switch:disabled{opacity:.45;cursor:default} --.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} --.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} --.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} --.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} --.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} --.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} --@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} --/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below -- 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and -- keeps its own words. Nothing is clipped to fit a width that is not there. */ --@container bt-panel (max-width:300px){ -- .bt-bs-row{flex-wrap:wrap;row-gap:6px} -- .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} -- .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} --} --`; -- --// src/client/group-settings.js --var import_react30 = require("react"); --var ROOM_MODES = [ -- ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], -- ["everyone", "Everyone", "Every member answers in turn, admin first."] --]; --var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; --function faceSeatBot(seat, roster) { -- if (!seat) return void 0; -- const record = seat.record && typeof seat.record === "object" ? seat.record : {}; -- const extra = {}; -- if (typeof record.color === "string" && record.color) extra.color = record.color; -- if (record.avatar && typeof record.avatar === "object") extra.avatar = record.avatar; -- if (seat.local && !seat.local.remote) return Object.keys(extra).length ? { ...seat.local, ...extra } : seat.local; -- const handle = typeof record.handle === "string" && record.handle.trim() ? record.handle : typeof seat.key === "string" && seat.key.startsWith("@") ? seat.key : ""; -- return lookBotForRemote({ -- handle, -- displayName: record.displayName || seat.bot, -- name: seat.bot, -- label: record.label, -- id: record.id || seat.key, -- ...extra -- }, roster); --} --function faceMemberLook(bot) { -- if (!bot) return void 0; -- return bot.remote ? lookAvoidingLocals(bot) : lookOf(bot); --} --function faceMarks(person, roster, size) { -- const marks = []; -- for (const seat of person?.seats ?? []) { -- const bot = faceSeatBot(seat, roster); -- if (!bot) continue; -- marks.push((0, import_react30.createElement)(BotMark, { key: seat.key, bot, size, live: false })); -- } -- if (marks.length === 0) return [(0, import_react30.createElement)(PersonMark, { key: "person", size, name: person?.name })]; -- return marks; --} --function faceRoomCount(room2, roster) { -- const people2 = facePeople(room2, roster).length; -- const seats = faceSeats(room2, roster).length; -- const persons = people2 === 1 ? t("1 person") : t("{count} people", { count: people2 }); -- const bots = seats === 1 ? t("1 Bot") : t("{count} Bots", { count: seats }); -- return `${persons} · ${bots}`; --} --var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); --var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); --function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { -- const bots = person.seats.flatMap((seat, index) => { -- const name = seat.local && onOpenBot ? (0, import_react30.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react30.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); -- const marked = isLeadSeat(seat, lead) ? [name, (0, import_react30.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; -- return index === 0 ? marked : [(0, import_react30.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; -- }); -- return (0, import_react30.createElement)( -- "div", -- { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, -- (0, import_react30.createElement)( -- "span", -- { className: "bt-gs-who bt-face-person" }, -- (0, import_react30.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), -- (0, import_react30.createElement)( -- "span", -- { className: "bt-face-person-text" }, -- (0, import_react30.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), -- (0, import_react30.createElement)("span", { className: "bt-face-bots" }, bots) -- ) -- ) -- ); -+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} -+.bt-model-test-result[data-ok=true]{color:#1a7f37} -+.bt-model-test-result[data-ok=false]{color:#c21d2e} -+.bt-model-test-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)} -+/* #1347: the hint beside "Test connection" is prose and wraps; the button next to it keeps its own -+ words ("Test connection" on one line) and takes the room it needs. */ -+.bt-model-test>.bt-set-copy{flex:1 1 12ch;min-width:12ch} -+.bt-model-test>.bt-soft{flex:none;white-space:nowrap} -+@container bt-panel (max-width:300px){ -+ .bt-model-test{flex-wrap:wrap;row-gap:6px} -+ .bt-model-test>.bt-set-copy{flex:1 1 100%} - } --function FaceLead({ room: room2, roster, pick: pick2 }) { -- const lead = room2.face?.lead; -- const [open, setOpen] = (0, import_react30.useState)(false); -- const [busy, setBusy] = (0, import_react30.useState)(false); -- const [error, setError] = (0, import_react30.useState)(""); -- const seats = faceSeats(room2, roster); -- const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); -- if (!current) return null; -- const canPick = lead.canPick === true && typeof pick2 === "function"; -- const choose = async (handle) => { -- setBusy(true); -- setError(""); -- try { -- await pick2(handle); -- setOpen(false); -- } catch (err) { -- setError(err?.message || t("Could not change the room lead.")); -- } finally { -- setBusy(false); -+`; -+ -+// src/client/folder-menu.js -+var import_react24 = require("react"); -+var import_react_dom9 = require("react-dom"); -+var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); -+var Tick = ({ on }) => (0, import_react24.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react24.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_react24.useState)(null); -+ const [view, setView] = (0, import_react24.useState)("list"); -+ const [draft, setDraft] = (0, import_react24.useState)(""); -+ const [listing, setListing] = (0, import_react24.useState)(null); -+ const [problem, setProblem] = (0, import_react24.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(); - }; -- return (0, import_react30.createElement)( -+ 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_react24.createElement)( - "div", -- { className: "bt-face-lead", "data-flybot": "face-lead" }, -- (0, import_react30.createElement)("div", { className: "bt-section-title" }, t("Room lead")), -- (0, import_react30.createElement)( -- "div", -- { className: "bt-set-row" }, -- (0, import_react30.createElement)( -+ { key: "list", className: "bt-folder-view" }, -+ (0, import_react24.createElement)( -+ "button", -+ { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, -+ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, fallback), -+ (0, import_react24.createElement)(Tick, { on: !value }) -+ ), -+ folders.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -+ folders.map((folder) => (0, import_react24.createElement)( -+ "button", -+ { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, -+ (0, import_react24.createElement)("span", { className: "bt-folder-copy" }, (0, import_react24.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react24.createElement)("span", { className: "bt-folder-path" }, folder.path)), -+ (0, import_react24.createElement)(Tick, { on: folder.path === value }) -+ )), -+ (0, import_react24.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ info?.picker === "native" ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -+ info?.picker === "browse" ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -+ (0, import_react24.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -+ setDraft(value ?? ""); -+ setView("type"); -+ } }, (0, import_react24.createElement)(PencilIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) -+ ); -+ const typeView = (0, import_react24.createElement)( -+ "div", -+ { key: "type", className: "bt-folder-view" }, -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react24.createElement)(ChevronLeftIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react24.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_react24.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_react24.createElement)( -+ "div", -+ { key: `browse:${listing.path}`, className: "bt-folder-view" }, -+ (0, import_react24.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react24.createElement)(ChevronLeftIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react24.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -+ parent ? (0, import_react24.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react24.createElement)(FolderIcon), (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -+ inside.map((entry) => (0, import_react24.createElement)( -+ "button", -+ { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, -+ (0, import_react24.createElement)(FolderIcon), -+ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, entry.name), -+ (0, import_react24.createElement)(ChevronRightIcon) -+ )), -+ inside.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -+ listing.truncated ? (0, import_react24.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -+ (0, import_react24.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react24.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_react24.createElement)( -+ import_react24.Fragment, -+ null, -+ (0, import_react24.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_react24.createElement)( - "span", -- { className: "bt-set-copy" }, -- (0, import_react30.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), -- (0, import_react30.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) -+ { className: "bt-model-copy" }, -+ (0, import_react24.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -+ value ? (0, import_react24.createElement)("span", { className: "bt-model-provider" }, value) : null - ), -- canPick ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null -+ (0, import_react24.createElement)(Caret) - ), -- open && canPick ? seats.map((seat) => (0, import_react30.createElement)( -+ shownPlace ? (0, import_react_dom9.createPortal)((0, import_react24.createElement)( - "div", -- { key: seat.key, className: "bt-set-row" }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), -- seat === current ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) -- )) : null, -- open && canPick && lead.picked ? (0, import_react30.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, -- canPick || !lead.supported ? null : (0, import_react30.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), -- error ? (0, import_react30.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null -+ { 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_react24.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null -+ ), document.body) : null - ); - } --function seatedCount(room2) { -- const live = room2?.presentedParticipants ?? room2?.participants ?? []; -- const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length; -- const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length; -- return humans + remotes + (room2?.members?.length ?? 0); -+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_react25 = require("react"); -+var POLL_MS4 = 3e4; -+var shared = null; -+var listeners = /* @__PURE__ */ new Set(); -+function publish(view) { -+ shared = view; -+ for (const listener of listeners) listener(view); - } --function roomSubtitle(room2, roster) { -- if (room2.origin === "face-to-face") return faceRoomCount(room2, roster); -- const admin = roster.byId[room2.admin]; -- const count = seatedCount(room2); -- return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); -+function useSchedules(actions) { -+ const [view, setView] = (0, import_react25.useState)(shared); -+ (0, import_react25.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_MS4); -+ }); -+ load(); -+ return () => { -+ stopped = true; -+ clearTimeout(timer); -+ listeners.delete(setView); -+ }; -+ }, []); -+ return view; - } --function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { -- const [adding, setAdding] = (0, import_react30.useState)(false); -- const [confirming, setConfirming] = (0, import_react30.useState)(false); -- (0, import_react30.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 4e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const live = room2.presentedParticipants ?? room2.participants ?? []; -- const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -- const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const face = room2.origin === "face-to-face"; -- const people2 = face ? facePeople(room2, roster) : []; -- const shared2 = humans.length > 1; -- const chiefs = new Set((roster.humans ?? []).map((person) => person.designatedChiefId).filter(Boolean)); -- const outside = roster.bots.filter((bot) => { -- if (bot.remote) return false; -- if (room2.members.includes(bot.id)) return false; -- if (shared2) return chiefs.has(bot.id); -- return true; -- }); -- const outsidePeople = (roster.humans ?? []).filter((person) => !humans.some((p) => p.id === person.id)); -- const outsideRemotes = (roster.remoteBots ?? []).filter((bot) => !remotes.some((p) => p.handle === bot.handle)); -- const admin = roster.byId[room2.admin]; -- const mode = shownMode(room2, roster); -- const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; -- const update = (patch) => run(actions.updateRoom(room2.id, patch)); -- const openBot = (id) => { -- actions.closeSettings(); -- actions.openSession(id); -- }; -+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 weekdayNames(days) { -+ const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); -+ return names.join(dateLocale().startsWith("zh") ? "、" : ", "); -+} -+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 }); -+ if (seconds % 60 === 0) return t("Every {count} minutes", { count: seconds / 60 }); -+ return t("Every {count} seconds", { count: seconds }); -+} -+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 text2 = 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 `${text2}${zone}`; -+ } -+ case "cron": -+ return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -+ default: -+ return ""; -+ } -+} -+var nextText = (task) => task.status === "paused" ? t("Paused") : task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); -+function lastRunText(run) { -+ if (run === void 0 || !Number.isFinite(run.at)) return ""; -+ const time = dayAndTime(run.at); -+ switch (run.result) { -+ case "running": -+ return t("Running since {time}", { time }); -+ case "stopped": -+ return t("Last run {time} · Stopped", { time }); -+ case "failed": -+ return t("Last run {time} · Failed: {reason}", { time, reason: hostText(run.reason || "Unknown error") }); -+ default: -+ return t("Last run {time}", { time }); -+ } -+} -+function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) { -+ const [confirming, setConfirming] = (0, import_react25.useState)(false); -+ const [busy, setBusy] = (0, import_react25.useState)(false); -+ (0, import_react25.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(false), 3e3); -+ return () => clearTimeout(timer); -+ }, [confirming]); - const remove = () => { - if (!confirming) { - setConfirming(true); - return; - } -- void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { -- if (done) onDeleted?.(); -+ setBusy(true); -+ void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); -+ }; -+ const toggle = () => { -+ setBusy(true); -+ const step = task.status === "paused" ? actions.scheduleResume : actions.schedulePause; -+ void step(task.sessionId, task.id).then((next) => { -+ setBusy(false); -+ onView(next); -+ }, () => setBusy(false)); -+ }; -+ const [retryError, setRetryError] = (0, import_react25.useState)(""); -+ const retry = () => { -+ setBusy(true); -+ setRetryError(""); -+ void actions.scheduleRetry(task.sessionId, task.id).then((next) => { -+ setBusy(false); -+ onView(next); -+ }, (error) => { -+ setBusy(false); -+ setRetryError(hostText(error?.message ?? String(error))); - }); - }; -- return (0, import_react30.createElement)( -+ const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; -+ const stoppable = task.status === "active" || task.status === "paused"; -+ const failed = task.lastRun?.result === "failed"; -+ const last = lastRunText(task.lastRun); -+ return (0, import_react25.createElement)( - "div", -- { className: "bt-bs" }, -- (0, import_react30.createElement)( -- "div", -- { className: "bt-bs-hero" }, -- (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -- (0, import_react30.createElement)( -- "div", -- { className: "bt-bs-who" }, -- (0, import_react30.createElement)("span", { className: "bt-bs-name" }, room2.name), -- (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -- ) -+ { className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, -+ showBot ? (0, import_react25.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -+ (0, import_react25.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react25.createElement)("span", { className: "bt-task-title" }, task.title), -+ (0, import_react25.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_react25.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, -+ last ? (0, import_react25.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, -+ retryError ? (0, import_react25.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null - ), -- (0, import_react30.createElement)( -- Card, -- { title: t("Profile") }, -- (0, import_react30.createElement)( -- Row, -- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -- (0, import_react30.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) -- ), -- (0, import_react30.createElement)( -- Row, -- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -- (0, import_react30.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) -+ (0, import_react25.createElement)("span", { className: "bt-task-next" }, nextText(task)), -+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, -+ onEdit ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react25.createElement)(PencilIcon)) : null, -+ onCopy ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react25.createElement)(DuplicateIcon)) : null, -+ stoppable ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, -+ (0, import_react25.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_react25.createElement)(TrashIcon)) -+ ); -+} -+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 = [["seconds", 1, "seconds"], ["minutes", 60, "minutes"], ["hours", 3600, "hours"], ["days", 86400, "days"]]; -+function localInput(ms) { -+ const date = new Date(ms); -+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; -+} -+function formStart(task) { -+ const seconds = task?.kind === "every" ? Number(task.everySeconds) : 3600; -+ const [unit, size] = seconds % 86400 === 0 ? ["days", 86400] : seconds % 3600 === 0 ? ["hours", 3600] : seconds % 60 === 0 ? ["minutes", 60] : ["seconds", 1]; -+ const once = task?.kind === "at" || task?.kind === "after"; -+ return { -+ title: task?.title ?? "", -+ prompt: task?.prompt ?? "", -+ kind: once ? "at" : task?.kind ?? "daily", -+ at: once && !Number.isNaN(Date.parse(task.scheduledAt)) ? localInput(Date.parse(task.scheduledAt)) : inAnHour(), -+ time: task?.time ? clock2(task.time) : "09:00", -+ weekdays: task?.kind === "weekly" && Array.isArray(task.weekdays) ? task.weekdays : [1, 2, 3, 4, 5], -+ count: Math.max(1, Math.round(seconds / size)), -+ unit, -+ timeZone: task?.timeZone || localZone() -+ }; -+} -+function TaskForm({ roster, actions, start, task, onDone, onCancel }) { -+ const first = formStart(task); -+ const [target, setTarget] = (0, import_react25.useState)(task ? task.botId ?? task.roomId : start ?? ""); -+ const [title, setTitle] = (0, import_react25.useState)(first.title); -+ const [prompt, setPrompt] = (0, import_react25.useState)(first.prompt); -+ const [kind, setKind] = (0, import_react25.useState)(first.kind); -+ const [at, setAt] = (0, import_react25.useState)(first.at); -+ const [time, setTime] = (0, import_react25.useState)(first.time); -+ const [weekdays, setWeekdays] = (0, import_react25.useState)(first.weekdays); -+ const [count, setCount] = (0, import_react25.useState)(first.count); -+ const [unit, setUnit] = (0, import_react25.useState)(first.unit); -+ const [busy, setBusy] = (0, import_react25.useState)(false); -+ const [error, setError] = (0, import_react25.useState)(""); -+ const timing = () => { -+ if (kind === "cron") return { kind: "keep" }; -+ if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone }; -+ if (kind === "every") return { kind, seconds: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; -+ return { kind, time, timeZone: first.timeZone, ...kind === "weekly" ? { weekdays } : {} }; -+ }; -+ const submit = (event) => { -+ event.preventDefault(); -+ if (busy || target === "") return; -+ setBusy(true); -+ setError(""); -+ let when3; -+ try { -+ when3 = timing(); -+ } catch { -+ setError(t("Pick a date and time")); -+ setBusy(false); -+ return; -+ } -+ const saved = task ? actions.scheduleUpdate({ sessionId: task.sessionId, id: task.id, target, title, prompt, when: when3 }) : actions.scheduleCreate({ target, title, prompt, when: when3 }); -+ void saved.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_react25.createElement)( -+ "form", -+ { className: "bt-set-card bt-task-form", onSubmit: submit }, -+ (0, import_react25.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -+ (0, import_react25.createElement)( -+ "select", -+ { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, -+ (0, import_react25.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -+ (0, import_react25.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react25.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -+ roster.rooms.length > 0 ? (0, import_react25.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react25.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null - ) - ), -- (0, import_react30.createElement)( -- Card, -- { title: t("Who answers") }, -- (0, import_react30.createElement)( -+ (0, import_react25.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react25.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) -+ ), -+ (0, import_react25.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -+ (0, import_react25.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_react25.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("When"))), -+ (0, import_react25.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react25.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -+ (0, import_react25.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react30.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react30.createElement)("button", { -- key: id, -+ { key: kind, className: "bt-task-when bt-veil" }, -+ kind === "cron" ? (0, import_react25.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, -+ kind === "at" ? (0, import_react25.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_react25.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react25.createElement)("button", { -+ key: day, - 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_react30.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -+ 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_react25.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_react25.createElement)( -+ import_react25.Fragment, -+ null, -+ (0, import_react25.createElement)("span", { className: "bt-task-every" }, t("Every")), -+ (0, import_react25.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_react25.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_react25.createElement)("option", { key: id, value: id }, t(label))) -+ ) -+ ) : null - ) - ), -- (0, import_react30.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react30.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react30.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -+ error ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ (0, import_react25.createElement)( -+ "div", -+ { className: "bt-set-row bt-task-foot" }, -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react25.createElement)( -+ "button", -+ { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, -+ task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") - ) -- ), -- face ? (0, import_react30.createElement)( -- Card, -- { title: t("Members · {count}", { count: people2.length }) }, -- people2.map((person) => (0, import_react30.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) -- ) : null, -- face ? null : (0, import_react30.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, -- humans.map((person) => (0, import_react30.createElement)( -- "div", -- { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react30.createElement)( -- "span", -- { className: "bt-gs-who" }, -- (0, import_react30.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), -- (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Human")), -- person.role === "admin" ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null -- )), -- members.map((member) => { -- const leads = room2.admin === member.id; -- const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; -- const taskLead = room2.taskLead === member.id; -- return (0, import_react30.createElement)( -- "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react30.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react30.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react30.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, -- taskLead ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, -- (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Chief")) : null -- ), -- leads ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- boardHere ? taskLead ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, -- members.length > 2 ? (0, import_react30.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 -- ); -- }), -- remotes.map((person) => (0, import_react30.createElement)( -+ ) -+ ); -+} -+function openPlugins(actions) { -+ actions.closeSettings(); -+ actions.selectPanel("plugins"); -+} -+function AutomationOff({ actions }) { -+ return (0, import_react25.createElement)( -+ "div", -+ { className: "bt-set-card bt-task-off" }, -+ (0, import_react25.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react25.createElement)(ClockIcon)), -+ (0, import_react25.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) -+ ); -+} -+function QuietHoursCard({ view, actions }) { -+ const quiet = view.quietHours ?? { on: false }; -+ const [busy, setBusy] = (0, import_react25.useState)(false); -+ const [error, setError] = (0, import_react25.useState)(""); -+ const [from, setFrom] = (0, import_react25.useState)(quiet.from ?? "22:00"); -+ const [to, setTo] = (0, import_react25.useState)(quiet.to ?? "07:00"); -+ const unchecked = view.quietUnchecked === true; -+ const refresh = () => { -+ void Promise.resolve(actions.schedules?.()).then((next) => { -+ if (next) publish(next); -+ }).catch(() => { -+ }); -+ }; -+ const save = (next) => { -+ if (busy) return; -+ setBusy(true); -+ setError(""); -+ const shown = next.on ? { on: true, from: next.from ?? from, to: next.to ?? to } : { on: false }; -+ void Promise.resolve(actions.setPrefs({ quietHours: next })).then(() => { -+ publish({ ...view, quietHours: shown }); -+ refresh(); -+ }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); -+ }; -+ return (0, import_react25.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react25.createElement)("h3", null, t("Quiet hours")), -+ (0, import_react25.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ (0, import_react25.createElement)( - "div", -- { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react30.createElement)( -- "span", -- { className: "bt-gs-who" }, -- (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -- (0, import_react30.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), -- // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is -- // labelled as such; a Bot with no Bot Social identity can never be written to at all. -- room2.origin === "face-to-face" ? null : (0, import_react30.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) -- ), -- // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. -- room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") -- )), -- room2.origin === "face-to-face" ? null : adding ? [ -- ...outsidePeople.map((person) => (0, import_react30.createElement)( -- "button", -- { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); -- } }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), -- (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add person")) -- )), -- ...outside.map((bot) => (0, import_react30.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_react30.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- ...outsideRemotes.map((bot) => (0, import_react30.createElement)( -- "button", -- { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); -- } }, -- (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), -- (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) -- )), -- (0, import_react30.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react30.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react30.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react30.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react30.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host's two-Bot floor guards membership changes; this hint warns before one. -- members.length + humans.length + remotes.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.")}${room2.origin === "face-to-face" || room2.origin === "inbound-share" ? "" : ` ${t("The task lead divides the board. When unset, the admin does.")}`}` -- )), -- // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after -- // the note on who leads, so that note stays with the members it is about (#892). -- room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react30.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) -- ), -- (0, import_react30.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), -- (0, import_react30.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react30.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_react30.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_react30.createElement)( -- Card, -- { title: t("More") }, -- (0, import_react30.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react30.createElement)( -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react25.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react30.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_react25.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), -+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) - ), -- (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) -- ) -+ (0, import_react25.createElement)("button", { -+ type: "button", -+ className: "bt-soft bt-soft-sm", -+ disabled: busy, -+ "aria-pressed": quiet.on, -+ onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) -+ }, quiet.on ? t("Turn off") : t("Turn on")) -+ ), -+ quiet.on ? (0, import_react25.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Window"))), -+ (0, import_react25.createElement)( -+ "div", -+ { className: "bt-task-when" }, -+ (0, import_react25.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), -+ (0, import_react25.createElement)("span", { className: "bt-task-every" }, "–"), -+ (0, import_react25.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) -+ ) -+ ) : null, -+ error ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ quiet.on && unchecked ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null - ) - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react30.useState)(null); -- const [members, setMembers] = (0, import_react30.useState)([]); -- const [name, setName] = (0, import_react30.useState)(""); -- const [busy, setBusy] = (0, import_react30.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)); -+function SchedulesPage({ roster, actions, start }) { -+ const view = useSchedules(actions); -+ const [adding, setAdding] = (0, import_react25.useState)(Boolean(start)); -+ const [editing, setEditing] = (0, import_react25.useState)(null); -+ if (view === null) return (0, import_react25.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -+ if (!view.available) return (0, import_react25.createElement)(AutomationOff, { actions }); -+ const shown = (next) => { -+ publish(next); -+ setAdding(false); -+ setEditing(null); - }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -+ const edit = (task) => { -+ setAdding(false); -+ setEditing(task); -+ }; -+ const byChat = [ -+ ...roster.bots.map((bot) => [bot.id, (0, import_react25.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_react25.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 [ -+ (0, import_react25.createElement)(QuietHoursCard, { key: "quiet", view, actions }), -+ adding ? (0, import_react25.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react25.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { -+ setEditing(null); -+ setAdding(true); -+ } }, (0, import_react25.createElement)(PlusIcon), t("New task"))), -+ byChat.length === 0 && !adding ? (0, import_react25.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react25.createElement)( -+ "section", -+ { key: id, className: "bt-set-section" }, -+ (0, import_react25.createElement)("h3", { className: "bt-task-head" }, avatar, name), -+ (0, import_react25.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) -+ )) -+ ]; -+} -+function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { -+ const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; -+ const [copying, setCopying] = (0, import_react25.useState)(null); -+ const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0; -+ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react25.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 })); -+ return (0, import_react25.createElement)( -+ import_react25.Fragment, -+ null, -+ open ? (0, import_react25.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, -+ source ? (0, import_react25.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => { -+ setCopying(null); -+ onDone(next); -+ }, onCancel: () => setCopying(null) }) : null, -+ rows.length + extra.length > 0 ? (0, import_react25.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null -+ ); -+} -+function CopyRow({ task, roster, actions, onView, onCancel }) { -+ const others = [ -+ ...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]), -+ ...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name]) -+ ]; -+ const [target, setTarget] = (0, import_react25.useState)(""); -+ const [busy, setBusy] = (0, import_react25.useState)(false); -+ const [error, setError] = (0, import_react25.useState)(""); -+ const submit = (event) => { -+ event.preventDefault(); -+ if (busy || target === "") return; - setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -+ setError(""); -+ void actions.scheduleCreate({ target, copyFrom: { sessionId: task.sessionId, id: task.id } }).then(onView, (failure) => { -+ setBusy(false); -+ setError(hostText(failure?.message ?? String(failure))); -+ }); - }; -- const chip = (bot, on, onClick, role) => (0, import_react30.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react30.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react30.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react30.createElement)(AdminStar) : null); -- return (0, import_react30.createElement)( -- "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react30.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react30.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react30.createElement)( -- "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) -- ) -+ return (0, import_react25.createElement)( -+ "form", -+ { className: "bt-set-row bt-task-copy", onSubmit: submit }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))), -+ (0, import_react25.createElement)( -+ "select", -+ { className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) }, -+ (0, import_react25.createElement)("option", { value: "" }, t("Pick a Bot or group chat")), -+ others.map(([id, name]) => (0, import_react25.createElement)("option", { key: id, value: id }, name)) - ), -- admin === null ? null : (0, import_react30.createElement)( -+ (0, import_react25.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ error ? (0, import_react25.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null -+ ); -+} -+function BotSchedules({ bot, roster, actions }) { -+ const view = useSchedules(actions); -+ const [editing, setEditing] = (0, import_react25.useState)(null); -+ if (view === null) return null; -+ const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; -+ return (0, import_react25.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react25.createElement)("h3", null, t("Scheduled tasks")), -+ !view.available ? (0, import_react25.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react30.createElement)( -+ { className: "bt-set-card" }, -+ (0, import_react25.createElement)( - "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) - ) -- ), -- admin === null || members.length === 0 ? null : (0, import_react30.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-ng-step" }, -- (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react30.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), -- (0, import_react30.createElement)( -- "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) -- ) -+ ) : (0, import_react25.createElement)(TaskList, { -+ tasks, -+ editing, -+ roster, -+ actions, -+ onEdit: setEditing, -+ onDone: (next) => { -+ publish(next); -+ setEditing(null); -+ }, -+ onCancel: () => setEditing(null), -+ extra: [(0, import_react25.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react25.createElement)(PlusIcon), t("New task"))] -+ }) - ); - } --var GROUP_SETTINGS_CSS = ` --.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} --.bt-ng-step{animation:bt-veil-in .22s ease-out} --.bt-ng-label{display:inline-flex;align-items:center;gap:6px} --.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} --.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} --.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-ng-chip:active{scale:.96} --.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} --.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} --.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} --.bt-gs-member{gap:8px} --.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} --.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} --.bt-gs-add{gap:8px;color:var(--bt-ink-2)} --.bt-face-marks{display:inline-flex;flex:none;align-items:center;gap:2px} --.bt-face-marks>.bt-mark{flex:none} --.bt-face-person{cursor:default} --.bt-face-person:hover .bt-row-title{text-decoration:none} --.bt-face-person-text{display:flex;flex-direction:column;min-width:0} --.bt-face-bots{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:16px;color:var(--bt-ink-3)} --.bt-face-bot{padding:0;border:0;background:none;color:inherit;font:inherit} --button.bt-face-bot{cursor:pointer} --button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-underline-offset:2px} --.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} --.bt-bs-stack .bt-seg{align-self:stretch} --@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} -+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_react25.createElement)( -+ "div", -+ null, -+ (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), -+ tasks.length === 0 ? (0, import_react25.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.") : t("No scheduled tasks yet. Add one")) : (0, import_react25.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react25.createElement)( -+ "button", -+ { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, -+ (0, import_react25.createElement)(ClockIcon), -+ (0, import_react25.createElement)("span", { className: "bt-task-line-title" }, task.title), -+ (0, import_react25.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) -+ ))) -+ ); -+} -+var SCHEDULE_CSS = ` -+.bt-task{gap:10px} -+.bt-task-copy{flex-wrap:wrap;gap:8px} -+.bt-task-copy .bt-task-bot{flex:1;min-width:140px;max-width:100%} -+.bt-task[data-done]{opacity:.6} -+.bt-task[data-busy]{opacity:.4} -+.bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} -+.bt-task-prompt{font-size:12px;line-height:16px;color:var(--bt-ink-2);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} -+.bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} -+.bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} -+.bt-task-remove svg{width:14px;height:14px} -+.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} -+.bt-task-tool svg{width:14px;height:14px} -+.bt-task-pause{height:26px;color:var(--bt-ink-2)} -+.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere} -+.bt-task-last[data-failed]{color:#c21d2e} -+.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)} -+.bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} -+.bt-task-head{display:flex;align-items:center;gap:6px} -+.bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -+.bt-task-bot{width:auto;min-width:160px;max-width:58%} -+.bt-task-when{display:flex;align-items:center;flex-wrap:wrap;gap:8px} -+.bt-task-when .bt-input{width:auto;height:32px} -+.bt-task-time{min-width:110px} -+.bt-task-count{width:72px!important} -+.bt-task-unit{width:auto;height:32px} -+.bt-task-every{font-size:13px;color:var(--bt-ink-2)} -+.bt-task-days{display:flex;gap:4px} -+.bt-task-day{width:30px;height:30px;border-radius:50%;border:1px solid var(--bt-line);background:none;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-task-day[aria-pressed=true]{background:var(--bt-user);border-color:var(--bt-user);color:var(--bt-user-ink)} -+.bt-task-foot{justify-content:flex-end;gap:8px} -+.bt-task-foot .bt-send{height:28px;font-size:12px} -+.bt-task-off{align-items:center;gap:8px;padding:28px 20px;text-align:center} -+.bt-task-off>*+*{border-top:0} -+.bt-task-off-mark{width:44px;height:44px;border-radius:50%;background:var(--bt-hover);display:flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} -+.bt-task-off-mark svg{width:20px;height:20px} -+.bt-task-off-title{font-size:14px;line-height:20px;font-weight:600} -+.bt-task-off .bt-set-hint{max-width:380px} -+.bt-task-list{display:flex;flex-direction:column;gap:2px} -+.bt-task-line{display:flex;align-items:center;gap:8px;width:100%;min-width:0;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:left;cursor:pointer;transition:background-color .12s ease} -+.bt-task-line:hover{background:var(--bt-hover)} -+.bt-task-line svg{flex:none;color:var(--bt-ink-3)} -+.bt-task-line-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-task-line-when{flex:none;font-size:12px;color:var(--bt-ink-3)} -+@media (prefers-reduced-motion:reduce){.bt-task-form{animation:none}} - `; - --// src/client/palette.js --var import_react31 = require("react"); --var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); --var paletteText = (raw, names) => readableLine(String(raw ?? ""), names); --var PALETTE_EXIT_MS = 160; --function CommandPalette({ start, token, roster, actions, useSessions }) { -- const [query, setQuery] = (0, import_react31.useState)(start ?? ""); -- const [cursor, setCursor] = (0, import_react31.useState)(0); -- const [leaving, setLeaving] = (0, import_react31.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 names = botNames(roster); -- 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 text2 = paletteText(raw, names); -- const at = text2.toLowerCase().indexOf(wanted); -- if (at < 0) continue; -- const from = Math.max(0, at - 24); -- hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.slice(from, at + wanted.length + 60)]); -- break; -- } -- } -+// src/client/capability-settings.js -+var import_react26 = require("react"); -+var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); -+var parseMap = (text2, label) => { -+ let value; -+ try { -+ value = JSON.parse(text2 || "{}"); -+ } catch { -+ throw new Error(t("{label} must be valid JSON.", { label })); -+ } -+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(t("{label} must be a JSON object.", { label })); -+ return value; -+}; -+var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); -+function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { -+ return (0, import_react26.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t(label))), -+ (0, import_react26.createElement)(multiline ? "textarea" : "input", { -+ className: multiline ? "bt-input bt-input-area" : "bt-input", -+ value, -+ disabled, -+ maxLength: label === "Name" ? 32 : void 0, -+ placeholder, -+ spellCheck: false, -+ onChange: (event) => onChange(event.target.value) -+ }) -+ ); -+} -+function CapabilityForm({ draft, setDraft, items, actions, onClose }) { -+ const [error, setError] = (0, import_react26.useState)(""); -+ const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); -+ const submit = async (event) => { -+ event.preventDefault(); -+ setError(""); -+ let payload; -+ try { -+ payload = { name: draft.name.trim(), kind: draft.kind, description: draft.description }; -+ if (draft.kind === "mcp" && draft.transport === "stdio") { -+ payload.transport = "stdio"; -+ payload.command = draft.command; -+ payload.args = draft.argsText.split("\n").filter((line2) => line2 !== ""); -+ payload.env = parseMap(draft.envText, t("Environment")); -+ } else if (draft.kind === "mcp") { -+ payload.transport = "streamable-http"; -+ payload.url = draft.url; -+ payload.headers = parseMap(draft.headersText, t("Headers")); -+ } else if (draft.kind === "skill") payload.content = draft.content; -+ else payload.items = draft.items; -+ } catch (failure) { -+ setError(failure?.message ?? String(failure)); -+ return; -+ } -+ try { -+ await actions.saveCapability(payload); -+ onClose(); -+ } catch (failure) { -+ setError(failure?.message ?? String(failure)); - } -- return JSON.stringify(hits); -- }); -- const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); -- const sections = []; -- const people2 = [ -- ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ -- key: bot.id, -- avatar: (0, import_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), -- title: pickerBotLabel(bot), -- 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_react31.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -- title: room2.name, -- subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), -- badge: room2.hidden ? t("Hidden") : void 0, -- run: async () => { -- if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); -- actions.openSession(room2.id); -- } -- })) -- ]; -- if (people2.length) sections.push([t("Bots"), people2]); -- const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ -- key: agent.id, -- avatar: (0, import_react31.createElement)("span", { className: "bt-palette-agent" }, (0, import_react31.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_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react31.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_react31.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -- { key: "cmd:bot", icon: (0, import_react31.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -- { key: "cmd:group", icon: (0, import_react31.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -- { key: "cmd:settings", icon: (0, import_react31.createElement)(GearIcon), title: t("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_react31.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(); - }; -- let index = -1; -- return (0, import_react31.createElement)( -- "div", -- { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -- (0, import_react31.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -- (0, import_react31.createElement)( -+ const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); -+ return (0, import_react26.createElement)( -+ "form", -+ { className: "bt-connector-form", onSubmit: submit }, -+ (0, import_react26.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), -+ (0, import_react26.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Type"))), -+ (0, import_react26.createElement)( -+ "select", -+ { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, -+ ["mcp", "skill", "pack"].map((kind) => (0, import_react26.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) -+ ) -+ ), -+ (0, import_react26.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), -+ draft.kind === "mcp" ? (0, import_react26.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), -+ (0, import_react26.createElement)( -+ "select", -+ { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, -+ (0, import_react26.createElement)("option", { value: "stdio" }, t("Local command")), -+ (0, import_react26.createElement)("option", { value: "streamable-http" }, t("HTTP server")) -+ ) -+ ) : null, -+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react26.createElement)( - "div", -- { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -- (0, import_react31.createElement)( -- "div", -- { className: "bt-palette-head" }, -- (0, import_react31.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)), -- (0, import_react31.createElement)("input", { -- 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_react31.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { -- setQuery(""); -- setCursor(0); -- } }, (0, import_react31.createElement)(CloseIcon)) : null -- ), -- (0, import_react31.createElement)( -- "div", -- { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, -- (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- rows.length === 0 ? (0, import_react31.createElement)( -- "div", -- { className: "bt-palette-empty" }, -- (0, import_react31.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)), -- (0, import_react31.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -- (0, import_react31.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -- ) : sections.map(([title, items]) => (0, import_react31.createElement)( -- import_react31.Fragment, -- { key: title }, -- (0, import_react31.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), -- items.map((row) => { -- index += 1; -- const at = index; -- return (0, import_react31.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_react31.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react31.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -- (0, import_react31.createElement)( -- "span", -- { className: "bt-palette-text" }, -- (0, import_react31.createElement)("span", { className: "bt-palette-title" }, row.title), -- row.subtitle ? (0, import_react31.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null -- ), -- row.badge ? (0, import_react31.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null -- ); -- }) -- )) -- ) -+ null, -+ (0, import_react26.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), -+ (0, import_react26.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), -+ (0, import_react26.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ ) : null, -+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react26.createElement)( -+ "div", -+ null, -+ (0, import_react26.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), -+ (0, import_react26.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ ) : null, -+ draft.kind === "skill" ? (0, import_react26.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, -+ draft.kind === "pack" ? (0, import_react26.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ available.length === 0 ? (0, import_react26.createElement)("div", { className: "bt-set-row" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react26.createElement)( -+ "label", -+ { key: item.name, className: "bt-set-row bt-bs-row" }, -+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, item.name)), -+ (0, import_react26.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) -+ )) -+ ) : null, -+ error ? (0, import_react26.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, -+ (0, import_react26.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), -+ (0, import_react26.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) - ) - ); - } -- --// src/client/bot-draft.js --function freshBotName(roster, base = "New Bot") { -- const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); -- if (!taken.has(base.toLowerCase())) return base; -- let index = 2; -- while (taken.has(`${base.toLowerCase()} ${index}`)) index += 1; -- return `${base} ${index}`; --} --function startModel(roster) { -- const models = roster.models ?? []; -- const served = (ref) => ref ? models.find((model) => model.ref === ref) : void 0; -- const main = roster.byId?.[roster.mainBotId] ?? (roster.bots ?? []).find((bot) => bot.id === roster.mainBotId); -- return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? ""; --} --function createPayload({ name, look, image, model, effort }) { -- const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } }; -- if (model) payload.model = model; -- if (effort) payload.effort = effort; -- return payload; --} --async function afterCreate({ id, text: text2, actions }) { -- actions.openSession(id); -- actions.showDetails(id, "settings"); -- const line2 = String(text2 ?? "").trim(); -- if (line2 === "") return; -- try { -- await actions.send(id, line2); -- } catch (failure) { -- actions.notify?.(failure?.message ?? String(failure)); -- } --} -- --// src/client/create-bot.js --var import_react32 = require("react"); --var pick = (list) => list[Math.floor(Math.random() * list.length)]; --var randomLook = (avoid = {}) => { -- const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); -- const colors = DEFAULT_COLORS.filter((color) => color !== avoid.color); -- return { shape: pick(shapes), color: pick(colors) }; --}; --function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { -- const [look, setLook] = (0, import_react32.useState)(() => randomLook()); -- const [image, setImage] = (0, import_react32.useState)(null); -- const [name, setName] = (0, import_react32.useState)(start?.name ?? ""); -- const [model, setModel] = (0, import_react32.useState)(() => startModel(roster)); -- const [effort, setEffort] = (0, import_react32.useState)(void 0); -- const [busy, setBusy] = (0, import_react32.useState)(false); -- const [error, setError] = (0, import_react32.useState)(""); -- const fileRef = (0, import_react32.useRef)(null); -- const frame = (0, import_react32.useRef)(null); -- useEscape(onBack); -- (0, import_react32.useEffect)(() => { -- if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); -- }, []); -- const fallbackName = freshBotName(roster, t("New Bot")); -- const finalName = name.trim() || fallbackName; -- const clash = roster.bots.some((bot) => bot.name.toLowerCase() === finalName.toLowerCase()); -- const problem = nameProblem(finalName); -- const create = async () => { -- if (busy || clash || problem) return; -- setError(""); -- setBusy(true); -- try { -- const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort })); -- await onCreated(bot); -- } catch (failure) { -- setError(failure?.message ?? String(failure)); -- setBusy(false); -- } -- }; -- const upload = (file) => { -- if (!file) return; -- setError(""); -- avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); -- }; -- const previewLook = image ? { ...look, image } : look; -- const card = (0, import_react32.createElement)( -+function CapabilityRow({ item, onEdit, onDelete, onApprove, confirming }) { -+ const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); -+ const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; -+ return (0, import_react26.createElement)( - "div", -- { className: "bt-create-card" }, -- (0, import_react32.createElement)( -+ { className: "bt-connector" }, -+ (0, import_react26.createElement)( - "div", -- { className: "bt-create-preview" }, -- // The key remounts the mark, so every new pick plays the pop. -- (0, import_react32.createElement)( -+ { className: "bt-connector-head" }, -+ (0, import_react26.createElement)( - "span", -- { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, -- (0, import_react32.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) -+ { className: "bt-connector-text" }, -+ (0, import_react26.createElement)("span", { className: "bt-connector-name" }, item.name), -+ (0, import_react26.createElement)("span", { className: "bt-connector-status" }, secondary) - ), -- (0, import_react32.createElement)("span", { className: "bt-create-name" }, finalName) -- ), -- (0, import_react32.createElement)("div", { className: "bt-create-step" }, t("Look")), -- (0, import_react32.createElement)( -- "div", -- { className: "bt-create-looks" }, -- (0, import_react32.createElement)(CharacterGrid, { -- look, -- hasPhoto: Boolean(image), -- onShape: (shape) => { -- setImage(null); -- setLook((current) => ({ ...current, shape })); -- }, -- onColor: (color) => { -- setImage(null); -- setLook((current) => ({ ...current, color })); -- } -- }), -- (0, import_react32.createElement)( -- "div", -- { className: "bt-actions bt-create-lookbar" }, -- (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -- setImage(null); -- setLook((current) => randomLook(current)); -- } }, t("Shuffle")), -- (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), -- image ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, -- (0, import_react32.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -- const file = event.target.files?.[0]; -- event.target.value = ""; -- upload(file); -- } }) -+ (0, import_react26.createElement)( -+ "span", -+ { className: "bt-connector-actions" }, -+ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, -+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), -+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) - ) -- ), -- (0, import_react32.createElement)( -- "label", -- { className: "bt-field" }, -- (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Name")), -- (0, import_react32.createElement)("input", { -- className: "bt-input", -- autoFocus: !inline, -- value: name, -- placeholder: fallbackName, -- maxLength: 40, -- "aria-label": t("Name"), -- onChange: (event) => setName(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter" && !event.nativeEvent.isComposing) { -- event.preventDefault(); -- void create(); -- } -- } -- }), -- problem ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null -- ), -- (0, import_react32.createElement)( -+ ) -+ ); -+} -+function CapabilitiesPage({ roster, actions, run }) { -+ const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name)); -+ const [draft, setDraft] = (0, import_react26.useState)(null); -+ const [confirming, setConfirming] = (0, import_react26.useState)(""); -+ (0, import_react26.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(""), 4e3); -+ return () => clearTimeout(timer); -+ }, [confirming]); -+ const startNew = () => setDraft(blank()); -+ const edit = (item) => setDraft({ -+ ...blank(), -+ ...item, -+ originalName: item.name, -+ argsText: (item.args ?? []).join("\n"), -+ envText: JSON.stringify(item.env ?? {}, null, 2), -+ headersText: JSON.stringify(item.headers ?? {}, null, 2), -+ items: [...item.items ?? []] -+ }); -+ const remove = (item) => { -+ if (confirming !== item.name) { -+ setConfirming(item.name); -+ return; -+ } -+ setConfirming(""); -+ void run(actions.deleteCapability(item.name)); -+ }; -+ return (0, import_react26.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react26.createElement)("h3", null, t("Shared capabilities")), -+ (0, import_react26.createElement)( - "div", -- { className: "bt-field" }, -- (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Model")), -- (0, import_react32.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { -- setModel(ref); -- setEffort(void 0); -- }, onPickEffort: setEffort }), -- (0, import_react32.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) -+ { className: "bt-actions" }, -+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) - ), -- error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -- (0, import_react32.createElement)( -+ draft ? (0, import_react26.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, -+ (0, import_react26.createElement)( - "div", -- { className: "bt-create-foot" }, -- // The dialog's header already has Back and Close (#230); only the inline form needs its own. -- inline ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, -- (0, import_react32.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) -+ { className: "bt-set-card bt-connectors-card" }, -+ items.length === 0 ? (0, import_react26.createElement)("div", { className: "bt-set-row" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react26.createElement)(CapabilityRow, { -+ key: item.name, -+ item, -+ confirming: confirming === item.name, -+ onEdit: () => edit(item), -+ onDelete: () => remove(item), -+ onApprove: () => void run(actions.approveCapability(item.name)) -+ })) - ) - ); -- if (inline) return (0, import_react32.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); -- return (0, import_react32.createElement)( -- "div", -- { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, -- (0, import_react32.createElement)( -- "div", -- { className: "bt-create-head" }, -- canGoBack ? (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.createElement)("span", { className: "bt-create-gap" }), -- (0, import_react32.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), -- (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react32.createElement)(CloseIcon)) -- ), -- (0, import_react32.createElement)("div", { className: "bt-create-body" }, card) -+} -+function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { -+ const items = [...roster.capabilities ?? []].sort((a, b) => Number(b.kind === "pack") - Number(a.kind === "pack") || a.name.localeCompare(b.name)); -+ const attached = new Set(bot.capabilities ?? []); -+ const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; -+ if (items.length === 0) return null; -+ return (0, import_react26.createElement)( -+ Card2, -+ { title: t("Capabilities") }, -+ items.map((item) => (0, import_react26.createElement)(SwitchRow2, { -+ key: item.name, -+ label: item.name, -+ hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -+ on: attached.has(item.name), -+ onChange: (enabled) => void run(actions.setBotCapability(bot.id, item.name, enabled)) -+ })) - ); - } - --// src/client/new-chat.js --var import_react33 = require("react"); --function NewChat({ mode, roster, actions }) { -- const [query, setQuery] = (0, import_react33.useState)(""); -- const [picked, setPicked] = (0, import_react33.useState)([]); -- const [group, setGroup] = (0, import_react33.useState)(mode === "group"); -- const [compose, setCompose] = (0, import_react33.useState)(mode !== "new"); -- const [admin, setAdmin] = (0, import_react33.useState)(null); -- const [message, setMessage] = (0, import_react33.useState)(""); -- const [busy, setBusy] = (0, import_react33.useState)(false); -- const [error, setError] = (0, import_react33.useState)(""); -- const [fix, setFix] = (0, import_react33.useState)(null); -- const [fixNote, setFixNote] = (0, import_react33.useState)(""); -- const [inviteLink, setInviteLink] = (0, import_react33.useState)(""); -- const [cursor, setCursor] = (0, import_react33.useState)(0); -- const [draft, setDraft] = (0, import_react33.useState)(mode === "create" ? { name: "", fromList: false } : null); -- const inputRef = (0, import_react33.useRef)(null); -- const close = () => actions.closeOverlay(); -- useEscape(close); -- const paneLeft = usePaneLeft(); -- const wanted = query.trim().toLowerCase(); -- const bots = listedChatBots(roster); -- const choosingAdmin = group && admin === null; -- const pool = choosingAdmin ? bots.filter((bot) => !bot.remote) : bots; -- const matches = pool.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || pickerBotLabel(bot).toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); -- const exact = bots.some((bot) => bot.name.toLowerCase() === wanted); -- const pickedBots = picked.map((id) => roster.byId[id]).filter(Boolean); -- const run = async (task) => { -- if (busy) return; -- setError(""); -- setFix(null); -- setFixNote(""); -- setInviteLink(""); -- setBusy(true); -- try { -- await task(); -- } catch (failure) { -- setError(failure?.message ?? String(failure)); -- setFix(ungrantedLocalFix(failure?.details) ?? null); -- } finally { -- setBusy(false); -+// src/client/connector-bots.js -+var import_react27 = require("react"); -+function ownBots(roster) { -+ const me = roster?.self?.memberId; -+ return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); -+} -+var connectedConnectors = (roster) => (roster?.connectors ?? []).filter((connector) => connector.state !== "off" && connector.kind !== "sender" && !(connector.kind === "channel" && connector.mode !== "app")); -+var isOn = (bot, id) => bot.connectors?.[id] !== false; -+function ownHint(bot, id) { -+ return isOn(bot, id) ? t("On. This Bot can use it on every turn.") : t("Off. This Bot will not use it."); -+} -+function usageText(roster, id) { -+ const bots = ownBots(roster); -+ const own = bots.filter((bot) => isOn(bot, id)).length; -+ return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); -+} -+function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { -+ return (0, import_react27.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); -+} -+function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { -+ const bots = ownBots(roster); -+ return (0, import_react27.createElement)( -+ "div", -+ { className: "bt-connector-bots" }, -+ bots.length === 0 ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react27.createElement)( -+ "div", -+ { key: bot.id, className: "bt-connector-bot" }, -+ (0, import_react27.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) -+ )), -+ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) -+ ); -+} -+function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { -+ const connected = connectedConnectors(roster); -+ if (connected.length === 0) return null; -+ return (0, import_react27.createElement)( -+ Card2, -+ { title: t("Connectors") }, -+ connected.map((connector) => (0, import_react27.createElement)( -+ "div", -+ { key: connector.id, className: "bt-connector-bot" }, -+ (0, import_react27.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) -+ )) -+ ); -+} -+ -+// src/client/memory-panel.js -+var import_react28 = require("react"); -+var POLL_MS5 = 1500; -+var MENU_EXIT_MS2 = 120; -+var counts = /* @__PURE__ */ new Map(); -+var countListeners = /* @__PURE__ */ new Set(); -+function noteCount(botId, count) { -+ if (counts.get(botId) === count) return; -+ counts.set(botId, count); -+ for (const listener of countListeners) listener(); -+} -+function useMemoryCount(botId, actions) { -+ const [count, setCount] = (0, import_react28.useState)(counts.get(botId) ?? null); -+ (0, import_react28.useEffect)(() => { -+ const listener = () => setCount(counts.get(botId) ?? null); -+ countListeners.add(listener); -+ listener(); -+ let stop = false; -+ void actions.memoryView?.(botId).then((view) => { -+ if (!stop) noteCount(botId, view.entries.length); -+ }).catch(() => { -+ }); -+ return () => { -+ stop = true; -+ countListeners.delete(listener); -+ }; -+ }, [botId]); -+ return count; -+} -+function MemoryRow({ bot, actions }) { -+ const count = useMemoryCount(bot.id, actions); -+ const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -+ return (0, import_react28.createElement)( -+ "div", -+ null, -+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Memory")), -+ (0, import_react28.createElement)( -+ "button", -+ { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -+ (0, import_react28.createElement)(MemoryIcon), -+ (0, import_react28.createElement)("span", { className: "bt-mem-row-copy" }, label), -+ (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon)) -+ ) -+ ); -+} -+function ago(at, now) { -+ const seconds = Math.max(0, Math.round((now - at) / 1e3)); -+ if (seconds < 60) return null; -+ const format = new Intl.RelativeTimeFormat(dateLocale(), { numeric: "auto" }); -+ const minutes = Math.round(seconds / 60); -+ if (minutes < 60) return format.format(-minutes, "minute"); -+ const hours = Math.round(minutes / 60); -+ if (hours < 24) return format.format(-hours, "hour"); -+ return format.format(-Math.round(hours / 24), "day"); -+} -+function Skeleton({ label }) { -+ return (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-skel", "aria-busy": true }, -+ [0, 1].map((block) => (0, import_react28.createElement)( -+ "div", -+ { key: block, className: "bt-mem-skel-block" }, -+ (0, import_react28.createElement)("span", { className: "bt-mem-skel-head" }), -+ (0, import_react28.createElement)("span", null), -+ (0, import_react28.createElement)("span", null), -+ (0, import_react28.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) -+ )), -+ label ? (0, import_react28.createElement)("div", { className: "bt-mem-skel-label" }, label) : null -+ ); -+} -+function Summary({ bot, view, regenerate }) { -+ if (view.entries.length === 0) { -+ return (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-empty" }, -+ (0, import_react28.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react28.createElement)(MemoryIcon)), -+ (0, import_react28.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), -+ (0, import_react28.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) -+ ); -+ } -+ const failed = view.error && !view.summarizing ? (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-failed", role: "alert" }, -+ (0, import_react28.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), -+ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) -+ ) : null; -+ if (!view.summary) return (0, import_react28.createElement)(import_react28.Fragment, null, failed, failed ? null : (0, import_react28.createElement)(Skeleton, { label: t("Writing the summary…") })); -+ return (0, import_react28.createElement)( -+ import_react28.Fragment, -+ null, -+ failed, -+ (0, import_react28.createElement)( -+ "div", -+ { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, -+ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react28.createElement)("h3", { key: index }, block.text) : (0, import_react28.createElement)("p", { key: index }, block.text)) -+ ) -+ ); -+} -+var groupEntries = (entries2) => entries2.reduce((groups, entry) => { -+ const key = `${entry.scope}:${entry.topic ?? ""}`; -+ const group = groups.find((item) => item.key === key); -+ if (group) group.entries.push(entry); -+ else groups.push({ key, scope: entry.scope, topic: entry.topic, entries: [entry] }); -+ return groups; -+}, []); -+function EntryRow({ entry, remove }) { -+ const [confirming, setConfirming] = (0, import_react28.useState)(false); -+ const [busy, setBusy] = (0, import_react28.useState)(false); -+ (0, import_react28.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(false), 3e3); -+ return () => clearTimeout(timer); -+ }, [confirming]); -+ const by = entry.source === "memory panel" ? t("by you") : entry.by ? t("by {name}", { name: entry.by }) : null; -+ const meta = [entry.added, by].filter(Boolean).join(" · "); -+ const click = async () => { -+ if (!confirming) { -+ setConfirming(true); -+ return; - } -+ setBusy(true); -+ if (!await remove(entry)) setBusy(false); - }; -- const leaveOut = (id) => { -- if (id === admin) setAdmin(null); -- setPicked((current) => current.filter((other) => other !== id)); -+ return (0, import_react28.createElement)( -+ "li", -+ { className: "bt-mem-entry", "data-busy": busy || void 0 }, -+ (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-entry-copy" }, -+ (0, import_react28.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), -+ meta ? (0, import_react28.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null -+ ), -+ (0, import_react28.createElement)("button", { -+ type: "button", -+ className: "bt-mem-remove", -+ "data-confirm": confirming || void 0, -+ disabled: busy, -+ "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), -+ title: confirming ? void 0 : t("Remove from memory"), -+ onClick: click -+ }, confirming ? t("Remove") : (0, import_react28.createElement)(TrashIcon)) -+ ); -+} -+function Entries({ bot, view, remove }) { -+ const groups = groupEntries(view.entries); -+ const sections = [ -+ { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, -+ { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } -+ ]; -+ return (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-entries" }, -+ sections.map((section) => (0, import_react28.createElement)( -+ "section", -+ { key: section.scope, className: "bt-mem-section" }, -+ (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-section-head" }, -+ (0, import_react28.createElement)("h3", null, section.title), -+ section.hint ? (0, import_react28.createElement)("span", null, section.hint) : null, -+ (0, import_react28.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) -+ ), -+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react28.createElement)( -+ import_react28.Fragment, -+ { key: group.key }, -+ group.topic ? (0, import_react28.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, -+ (0, import_react28.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react28.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) -+ )) : (0, import_react28.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) -+ )) -+ ); -+} -+function changeLine(change) { -+ const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; -+ if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); -+ if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); -+ if (change.op === "edit") return t("Updated: {text}", { text: text2 }); -+ return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 }); -+} -+function Reply({ answer, dismiss }) { -+ return (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-reply", role: "status" }, -+ (0, import_react28.createElement)( -+ "div", -+ { className: "bt-mem-reply-head" }, -+ (0, import_react28.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), -+ answer.pending ? null : (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react28.createElement)(CloseIcon)) -+ ), -+ answer.pending ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react28.createElement)( -+ import_react28.Fragment, -+ null, -+ answer.reply ? (0, import_react28.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, -+ answer.changes.length > 0 ? (0, import_react28.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react28.createElement)( -+ "li", -+ { key: index, "data-ok": change.ok || void 0 }, -+ change.ok ? (0, import_react28.createElement)(CheckIcon) : (0, import_react28.createElement)(CloseIcon), -+ (0, import_react28.createElement)("span", null, changeLine(change)) -+ ))) : null -+ ) -+ ); -+} -+function useMemoryView(botId, actions) { -+ const [view, setView] = (0, import_react28.useState)(null); -+ const [error, setError] = (0, import_react28.useState)(""); -+ const timer = (0, import_react28.useRef)(0); -+ const latest = (0, import_react28.useRef)(0); -+ const alive = (0, import_react28.useRef)(true); -+ const show = (0, import_react28.useCallback)((next) => { -+ setView(next); - setError(""); -- setFix(null); -- setFixNote(""); -- setInviteLink(""); -- }; -- const copyInvite = async (bot) => { -- const social = globalThis.__flybot__?.social; -- let link; -+ noteCount(botId, next.entries.length); -+ }, [botId]); -+ const load = (0, import_react28.useCallback)(async (flags = {}) => { -+ clearTimeout(timer.current); -+ const ticket = ++latest.current; - try { -- const invite = await social.createInvite(bot.id); -- link = invite?.link || invite?.url; -- if (!link) throw new Error("no link"); -- } catch { -- setInviteLink(""); -- setFixNote(t("Could not create the invite. Use Bot settings → Sharing → Copy invite link for this Bot.")); -- return; -- } -- if (await copiedToClipboard(link)) { -- setInviteLink(""); -- setFixNote(t("Invite link for {name} copied. Send it to your friend; once they accept, create the group again.", { name: bot.name })); -- } else { -- setInviteLink(link); -- setFixNote(t("Could not copy the invite link for {name}. Copy the link below and send it to your friend; once they accept, create the group again.", { name: bot.name })); -+ const next = await actions.memoryView(botId, flags); -+ if (!alive.current || ticket !== latest.current) return; -+ show(next); -+ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5); -+ } catch (failure) { -+ if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure)); - } -+ }, [botId]); -+ (0, import_react28.useEffect)(() => { -+ alive.current = true; -+ void load({ summarize: true }); -+ return () => { -+ alive.current = false; -+ clearTimeout(timer.current); -+ }; -+ }, [botId]); -+ return { view, error, load, show }; -+} -+function useNow3(every) { -+ const [now, setNow] = (0, import_react28.useState)(Date.now()); -+ (0, import_react28.useEffect)(() => { -+ const id = setInterval(() => setNow(Date.now()), every); -+ return () => clearInterval(id); -+ }, [every]); -+ return now; -+} -+function MemoryDialog({ bot, actions, leaving = false }) { -+ const { view, error, load, show } = useMemoryView(bot.id, actions); -+ const [page, setPage] = (0, import_react28.useState)("summary"); -+ const [menu, setMenu] = (0, import_react28.useState)(false); -+ const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); -+ const [text2, setText] = (0, import_react28.useState)(""); -+ const [answer, setAnswer] = (0, import_react28.useState)(null); -+ const menuRef = (0, import_react28.useRef)(null); -+ const dotsRef = (0, import_react28.useRef)(null); -+ const inputRef = (0, import_react28.useRef)(null); -+ const now = useNow3(3e4); -+ const close = (0, import_react28.useCallback)(() => actions.closeMemory(), []); -+ useEscape(close); -+ (0, import_react28.useEffect)(() => { -+ if (!menu) return void 0; -+ const onDown = (event) => { -+ if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -+ setMenu(false); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ setMenu(false); -+ dotsRef.current?.focus(); -+ } -+ }; -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); -+ }; -+ }, [menu]); -+ const regenerate = () => { -+ setMenu(false); -+ setPage("summary"); -+ void load({ regenerate: true }); - }; -- const pick2 = (bot) => { -- setPicked((current) => [...current, bot.id]); -- setQuery(""); -- setCursor(0); -- inputRef.current?.focus(); -- }; -- const chooseAdmin = (bot) => { -- setAdmin(bot.id); -- setQuery(""); -- setCursor(0); -- inputRef.current?.focus(); -- }; -- const openBot = (bot) => run(async () => { -- if (choosingAdmin) { -- chooseAdmin(bot); -- return; -- } -- if (group || picked.length > 0) { -- pick2(bot); -- return; -- } -- if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); -- if (message.trim()) await actions.send(bot.id, message.trim()); -- actions.openSession(bot.id); -- close(); -- }); -- const createBot = () => { -- if (busy) return; -- setError(""); -- setDraft({ name: query.trim() !== "" && !exact ? query.trim() : "", fromList: true }); -- }; -- const created = (bot) => { -- close(); -- return afterCreate({ id: bot.id, text: message, actions }); -+ const openEntries = () => { -+ setMenu(false); -+ setPage("entries"); - }; -- const makeGroup = async () => { -- if (admin === null || picked.length === 0) return; -- const text2 = message.trim(); -- const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); -- const participants = remotes.length === 0 ? void 0 : [ -- ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), -- ...remotes -- ]; -- const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); -- if (text2) await actions.send(room2.id, text2); -- actions.openSession(room2.id); -- close(); -+ const backToSummary = () => { -+ setPage("summary"); -+ void load({ summarize: true }); - }; -- const createGroup = () => run(makeGroup); -- const sendMessage = () => run(async () => { -- const text2 = message.trim(); -- if (group && admin !== null) { -- await makeGroup(); -- return; -- } -- if (text2 === "" || picked.length === 0) return; -- if (picked.length === 1) { -- await actions.send(picked[0], text2); -- actions.openSession(picked[0]); -- close(); -- return; -- } -- const { members, remotes } = splitRoomPicks(roster, picked); -- const participants = remotes.length === 0 ? void 0 : [ -- ...members.map((id) => ({ kind: "bot", id })), -- ...remotes -- ]; -- const room2 = await actions.createRoom(members, participants ? { participants } : {}); -- await actions.send(room2.id, text2); -- actions.openSession(room2.id); -- close(); -- }); -- const menuOnly = mode === "new" && !compose && !group && picked.length === 0 && wanted === ""; -- const options = []; -- if (!group && picked.length === 0 && mode !== "search" && wanted === "") { -- if (menuOnly) { -- options.push({ key: "chat", icon: (0, import_react33.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { -- setCompose(true); -- setCursor(0); -- setTimeout(() => inputRef.current?.focus()); -- } }); -- } -- options.push({ key: "new", icon: (0, import_react33.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); -- options.push({ key: "group", icon: (0, import_react33.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { -- setGroup(true); -- setCompose(true); -- setCursor(0); -- inputRef.current?.focus(); -- } }); -- for (const action of newChatActionRows({ mode, group, picked, query, roster })) { -- options.push({ -- key: action.key, -- icon: action.icon ? (0, import_react33.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react33.createElement)(PeopleIcon) }) : (0, import_react33.createElement)(PeopleIcon), -- label: t(action.label), -- hint: action.hint ? t(action.hint) : void 0, -- run: () => { -- const done = Promise.resolve().then(() => action.run({ close, actions, roster })); -- done.then((note) => { -- if (typeof note === "string" && note) actions.notify?.(note); -- }).catch((failure) => setError(failure?.message ?? String(failure))); -- } -- }); -- } -- if ((roster.agents ?? []).length === 0) { -- options.push({ key: "agent", icon: (0, import_react33.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { -- await actions.addAgent(); -- close(); -- }) }); -+ const remove = async (entry) => { -+ try { -+ show(await actions.memoryForget(bot.id, entry)); -+ return true; -+ } catch (failure) { -+ setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) }); -+ return false; - } -- } -- if (!menuOnly) { -- for (const bot of matches) { -- options.push({ key: bot.id, bot, label: pickerBotLabel(bot), hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); -+ }; -+ const ask = async (event) => { -+ event.preventDefault(); -+ const asked = text2.trim(); -+ if (asked === "" || answer?.pending) return; -+ setText(""); -+ setAnswer({ asked, pending: true }); -+ try { -+ const result = await actions.memoryAsk(bot.id, asked); -+ setAnswer({ asked, reply: result.reply, changes: result.changes }); -+ show(result.view); -+ if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" }); -+ } catch (failure) { -+ setAnswer({ asked, error: failure?.message ?? String(failure) }); - } -- } -- if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react33.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); -- const active = Math.min(cursor, Math.max(options.length - 1, 0)); -- const listed = options.map((option) => option.key).join("\n"); -- const glide = useGlide(`${active} --${listed}`, { axis: "y", ...QUICK_EDGES }); -- useFlip(glide.box, listed, "[data-flip]"); -- const drop = (0, import_react33.useRef)(null); -- useMorph(drop, listed); -- if (draft) { -- return (0, import_react33.createElement)( -- "div", -- { className: "bt-pane", style: { left: paneLeft } }, -- (0, import_react33.createElement)(CreateBot, { -- roster, -- actions, -- start: draft, -- canGoBack: draft.fromList, -- onBack: draft.fromList ? () => { -- setDraft(null); -- setTimeout(() => inputRef.current?.focus()); -- } : close, -- onCreated: created -- }) -- ); -- } -- const adminBot = admin === null ? void 0 : roster.byId[admin]; -- const recipients = adminBot ? [adminBot, ...pickedBots] : pickedBots; -- const placeholder = recipients.length > 0 ? t("Message {name}", { name: recipients.map((bot) => bot.name).join(t(" and ")) }) : t("Message Bot"); -- const unpick = () => { -- if (picked.length > 0) setPicked((current) => current.slice(0, -1)); -- else if (admin !== null) setAdmin(null); -+ inputRef.current?.focus(); - }; -- return (0, import_react33.createElement)( -+ const count = view?.entries.length ?? 0; -+ const when3 = view?.summary ? ago(view.summary.at, now) : null; -+ const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; -+ const busy = answer?.pending === true; -+ return (0, import_react28.createElement)( - "div", -- { -- className: "bt-pane", -- style: { left: paneLeft }, -- "data-menu": menuOnly || void 0, -- onMouseDown: menuOnly ? (event) => { -- if (event.target === event.currentTarget) close(); -- } : void 0 -- }, -- (0, import_react33.createElement)( -+ { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react28.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -+ (0, import_react28.createElement)( - "div", -- { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, -- // The popover needs no title row: Escape or a press outside closes it. -- menuOnly ? null : (0, import_react33.createElement)( -+ { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, -+ (0, import_react28.createElement)( - "div", -- { className: "bt-to" }, -- (0, import_react33.createElement)("span", { className: "bt-to-label" }, t("To:")), -- adminBot ? (0, import_react33.createElement)( -- "span", -- { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, -- (0, import_react33.createElement)(BotMark, { bot: adminBot, size: 16 }), -- adminBot.name, -- (0, import_react33.createElement)(AdminStar), -- (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") -- ) : null, -- choosingAdmin ? (0, import_react33.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react33.createElement)(AdminStar)) : null, -- pickedBots.map((bot) => (0, import_react33.createElement)( -- "span", -- { key: bot.id, className: "bt-token" }, -- (0, import_react33.createElement)(BotMark, { bot, size: 16 }), -- bot.name, -- (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") -- )), -- (0, import_react33.createElement)("input", { -- ref: inputRef, -- autoFocus: true, -- value: query, -- "aria-label": t("Recipient"), -- placeholder: mode === "search" ? t("Search Bots…") : choosingAdmin ? t("Choose the group admin") : group ? t("Who to chat with…") : t("Start a chat with…"), -- 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, options.length - 1)); -- } else if (event.key === "ArrowUp") { -- event.preventDefault(); -- setCursor(Math.max(active - 1, 0)); -- } else if (event.key === "Enter" && options[active]) { -- event.preventDefault(); -- options[active].run(); -- } else if (event.key === "Backspace" && query === "") unpick(); -- } -- }), -- group && admin !== null ? (0, import_react33.createElement)("button", { -- type: "button", -- className: "bt-send bt-to-create", -- disabled: busy || picked.length === 0, -- title: picked.length === 0 ? t("Add at least one more Bot") : void 0, -- onClick: createGroup -- }, t("Create group chat")) : null, -- (0, import_react33.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") -+ { className: "bt-mem-head" }, -+ page === "entries" ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react28.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react28.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -+ (0, import_react28.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -+ (0, import_react28.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react28.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react28.createElement)(DotsIcon)), -+ (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react28.createElement)(CloseIcon)), -+ menuShown ? (0, import_react28.createElement)( -+ "div", -+ { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, -+ (0, import_react28.createElement)( -+ "button", -+ { type: "button", role: "menuitem", onClick: openEntries }, -+ (0, import_react28.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -+ (0, import_react28.createElement)("span", { className: "bt-menu-hint" }, String(count)) -+ ), -+ (0, import_react28.createElement)( -+ "button", -+ { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, -+ (0, import_react28.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) -+ ) -+ ) : null - ), -- (0, import_react33.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react33.createElement)( -- "div", -- { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, -- (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- options.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react33.createElement)( -- "button", -- { -- key: option.key, -- type: "button", -- role: "option", -- className: "bt-option", -- "aria-selected": index === active, -- "data-flip": option.key, -- onMouseEnter: () => setCursor(index), -- onClick: option.run -- }, -- option.bot ? (0, import_react33.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react33.createElement)("span", { className: "bt-option-icon" }, option.icon), -- // An action row's description sits under its title and is always there (#283): a long line -- // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. -- // A Bot row's short hint stays beside the name and shows on the active row only. -- option.bot || !option.hint ? (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react33.createElement)("span", { className: "bt-option-copy" }, (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react33.createElement)("span", { className: "bt-option-sub" }, option.hint)), -- option.bot && option.hint && index === active ? (0, import_react33.createElement)("span", { className: "bt-option-hint" }, option.hint) : null -- )) -- )), -- menuOnly ? null : (0, import_react33.createElement)("div", { className: "bt-nc-spacer" }), -- error && fix ? (0, import_react33.createElement)( -+ (0, import_react28.createElement)( - "div", -- { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, -- (0, import_react33.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), -- (0, import_react33.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ -- typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react33.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { -- void copyInvite(bot); -- } }, t("Copy invite link for {name}", { name: bot.name })) : null, -- (0, import_react33.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) -- ])), -- fixNote ? (0, import_react33.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, -- inviteLink ? (0, import_react33.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null -- ) : error ? (0, import_react33.createElement)("div", { className: "bt-error" }, error) : null, -- menuOnly ? null : (0, import_react33.createElement)( -+ { className: "bt-mem-body" }, -+ error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -+ view === null ? error ? null : (0, import_react28.createElement)(Skeleton) : page === "entries" ? (0, import_react28.createElement)(Entries, { bot, view, remove }) : (0, import_react28.createElement)(Summary, { bot, view, regenerate }) -+ ), -+ (0, import_react28.createElement)( - "div", -- { className: "bt-nc-compose" }, -- (0, import_react33.createElement)( -- "div", -- { className: "bt-nc-card" }, -- (0, import_react33.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react33.createElement)(PlusIcon)), -- (0, import_react33.createElement)("textarea", { -- rows: 1, -- value: message, -- placeholder, -- "aria-label": placeholder, -- onChange: (event) => setMessage(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { -- event.preventDefault(); -- sendMessage(); -- } -- } -+ { className: "bt-mem-foot" }, -+ answer ? (0, import_react28.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -+ (0, import_react28.createElement)( -+ "form", -+ { className: "bt-mem-ask", onSubmit: ask }, -+ (0, import_react28.createElement)("input", { -+ ref: inputRef, -+ className: "bt-mem-input", -+ value: text2, -+ maxLength: 2e3, -+ disabled: view === null, -+ placeholder: t("Ask or update"), -+ "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), -+ onChange: (event) => setText(event.target.value) - }), -- (0, import_react33.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react33.createElement)(ArrowUpIcon)) -+ (0, import_react28.createElement)( -+ "button", -+ { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") }, -+ busy ? (0, import_react28.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react28.createElement)(ArrowUpIcon) -+ ) - ) - ) - ) - ); - } - --// src/client/exchange.js --var import_react34 = require("react"); --var SHEET_EXIT_MS = 220; --function exchangeText(text2) { -- if (typeof text2 !== "string") return ""; -- const CLOSE = ""; -- let body = text2; -- for (let previous; previous !== body; ) { -- previous = body; -- const open = body.indexOf(" open ? body.slice(body.indexOf(">", open) + 1, close) : body; -- body = stripFence(body.slice(0, open) + inner + (close > open ? body.slice(close + CLOSE.length) : "")); -+// src/client/standing-approvals.js -+var import_react29 = require("react"); -+var standingOf = (target, roster) => { -+ const me = roster?.self?.memberId; -+ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); -+}; -+function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card2 }) { -+ const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; -+ if (grants.length === 0) return null; -+ const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); -+ return (0, import_react29.createElement)( -+ Card2, -+ { title: t("Always allowed") }, -+ grants.map((grant) => (0, import_react29.createElement)( -+ "div", -+ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, -+ (0, import_react29.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), -+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) -+ ), -+ (0, import_react29.createElement)("button", { -+ type: "button", -+ className: "bt-soft bt-soft-sm", -+ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), -+ onClick: () => run(actions.removeStandingApproval(scope, target.id, grant.kind)) -+ }, t("Remove")) -+ )) -+ ); -+} -+ -+// src/client/bot-settings.js -+var import_react30 = require("react"); -+var capitalize = (word) => word[0].toUpperCase() + word.slice(1); -+async function avatarImage(file) { -+ if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -+ if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB")); -+ const bitmap = await createImageBitmap(file); -+ const side = 160; -+ const canvas = document.createElement("canvas"); -+ canvas.width = side; -+ canvas.height = side; -+ const context = canvas.getContext("2d"); -+ context.imageSmoothingQuality = "high"; -+ const crop = Math.min(bitmap.width, bitmap.height); -+ context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side); -+ bitmap.close?.(); -+ for (const quality of [0.9, 0.75, 0.6]) { -+ const url = canvas.toDataURL("image/webp", quality); -+ if (url.startsWith("data:image/webp") && url.length < 15e4) return url; - } -- return stripFence(stripTrace(body.replace(/^\s+/, ""))).trim(); -+ const png = canvas.toDataURL("image/png"); -+ if (png.length < 15e4) return png; -+ throw new Error(t("That image is too large")); - } --function ExchangeLine({ entry, roster, actions }) { -- const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; -- return (0, import_react34.createElement)( -+function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { -+ const [draft, setDraft] = (0, import_react30.useState)(null); -+ const reverting = (0, import_react30.useRef)(false); -+ const commit = () => { -+ const next = draft?.trim(); -+ setDraft(null); -+ if (reverting.current) { -+ reverting.current = false; -+ return; -+ } -+ if (next === void 0 || next === (value ?? "") || required && next === "") return; -+ void onSave(next); -+ }; -+ return (0, import_react30.createElement)(multiline ? "textarea" : "input", { -+ className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", -+ value: draft ?? value ?? "", -+ placeholder, -+ maxLength, -+ required, -+ "aria-label": label, -+ spellCheck: false, -+ onChange: (event) => setDraft(event.target.value), -+ onBlur: commit, -+ onKeyDown: (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ reverting.current = true; -+ event.currentTarget.blur(); -+ } -+ if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur(); -+ } -+ }); -+} -+function Row({ label, hint, stack = false, children }) { -+ return (0, import_react30.createElement)( -+ "label", -+ { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react30.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ children -+ ); -+} -+function Card({ title, children }) { -+ return (0, import_react30.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react30.createElement)("h3", null, title) : null, (0, import_react30.createElement)("div", { className: "bt-set-card" }, children)); -+} -+function Switch({ on, label, disabled, onChange }) { -+ return (0, import_react30.createElement)( -+ "button", -+ { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, -+ (0, import_react30.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) -+ ); -+} -+function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { -+ return (0, import_react30.createElement)( - "div", -- { className: "bt-group-msg" }, -- (0, import_react34.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), -- (0, import_react34.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react34.createElement)("span", { className: "bt-lead" }, (0, import_react34.createElement)(BotMark, { bot, size: 22 })) }) -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react30.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ (0, import_react30.createElement)(Switch, { on, label, disabled, onChange }) - ); - } --function ExchangeDialog({ pair, roster, actions }) { -- const [entries2, setEntries] = (0, import_react34.useState)(null); -- const [leaving, setLeaving] = (0, import_react34.useState)(false); -- const close = () => { -- if (leaving) return; -- setLeaving(true); -- setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS); -+function modelHint(bot, roster) { -+ const shown = modelOf(bot, roster); -+ if (shown === void 0) return t("Pick the model this Bot runs on."); -+ if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one."); -+ if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it."); -+ return t("Applies from the next message."); -+} -+function ShapeRing({ shape, size }) { -+ const maskId = (0, import_react30.useRef)(""); -+ if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; -+ const def = shapeOf(shape); -+ const unit = def.size / size; -+ const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; -+ return (0, import_react30.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); -+} -+function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { -+ const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; -+ if (!shapes.includes(look.shape)) shapes.push(look.shape); -+ const rows = []; -+ for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); -+ return (0, import_react30.createElement)( -+ import_react30.Fragment, -+ null, -+ (0, import_react30.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react30.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react30.createElement)("button", { -+ key: id, -+ type: "button", -+ className: "bt-shape", -+ "aria-label": t(shapeOf(id).label), -+ title: t(shapeOf(id).label), -+ "aria-pressed": !hasPhoto && look.shape === id, -+ onClick: () => onShape(id) -+ }, (0, import_react30.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react30.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -+ (0, import_react30.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react30.createElement)("button", { -+ key: color, -+ type: "button", -+ className: "bt-color", -+ "aria-label": t(capitalize(color)), -+ title: t(capitalize(color)), -+ "aria-pressed": look.color === color, -+ onClick: () => onColor(color) -+ }, (0, import_react30.createElement)("span", { style: { background: paintCss(color) } })))) -+ ); -+} -+function AvatarEditor({ bot, actions, run, onClose }) { -+ const look = lookOf(bot); -+ const [tab, setTab] = (0, import_react30.useState)(look.image ? "upload" : "bot"); -+ const [over, setOver] = (0, import_react30.useState)(false); -+ const ref = (0, import_react30.useRef)(null); -+ const fileRef = (0, import_react30.useRef)(null); -+ const upload = (file) => { -+ if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); - }; -- useEscape(close); -- (0, import_react34.useEffect)(() => { -- let stop = false; -- let timer; -- let seen = null; -- const load = async () => { -- const result = await actions.exchange(pair[0], pair[1]); -- if (stop) return; -- const key = JSON.stringify(result ?? []); -- if (key !== seen) { -- seen = key; -- setEntries(result ?? []); -+ (0, import_react30.useEffect)(() => { -+ const onDown = (event) => { -+ if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; -+ onClose(); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ onClose(); - } -- timer = setTimeout(load, document.hidden ? 8e3 : 2e3); - }; -- void load(); -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); - return () => { -- stop = true; -- clearTimeout(timer); -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); - }; -- }, [pair[0], pair[1]]); -- const left = roster.byId[pair[0]]; -- const right = roster.byId[pair[1]]; -- let lastDay = 0; -- const paneLeft = usePaneLeft(); -- return (0, import_react34.createElement)( -+ }, [onClose]); -+ (0, import_react30.useEffect)(() => { -+ if (tab !== "upload") return void 0; -+ const onPaste = (event) => { -+ const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -+ if (file) { -+ event.preventDefault(); -+ upload(file); -+ } -+ }; -+ window.addEventListener("paste", onPaste); -+ return () => window.removeEventListener("paste", onPaste); -+ }, [tab, bot]); -+ const hasPhoto = Boolean(look.image); -+ const glide = useGlide(tab); -+ return (0, import_react30.createElement)( - "div", -- { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, -- (0, import_react34.createElement)( -+ { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, -+ (0, import_react30.createElement)( - "div", -- { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, -- (0, import_react34.createElement)("div", { className: "bt-head" }, (0, import_react34.createElement)( -- "span", -- { className: "bt-pill", style: { cursor: "default" } }, -- left ? (0, import_react34.createElement)(BotMark, { bot: left, size: 22 }) : null, -- left?.name ?? "?", -- (0, import_react34.createElement)("span", { className: "bt-swap" }, "⇄"), -- right ? (0, import_react34.createElement)(BotMark, { bot: right, size: 22 }) : null, -- right?.name ?? "?" -- )), -- (0, import_react34.createElement)( -+ { className: "bt-avedit-top" }, -+ (0, import_react30.createElement)( - "div", -- { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, -- entries2 === null ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { -- const showDay = entry.time - lastDay > 30 * 60 * 1e3; -- lastDay = entry.time; -- return (0, import_react34.createElement)( -- import_react34.Fragment, -- { key: entry.id }, -- showDay ? (0, import_react34.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, -- (0, import_react34.createElement)(ExchangeLine, { entry, roster, actions }) -- ); -- }) -+ { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, -+ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react30.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) - ), -- (0, import_react34.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react34.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) -- ) -+ (0, import_react30.createElement)("button", { -+ type: "button", -+ className: "bt-avedit-tab bt-avedit-reset", -+ title: t("Reset character to default"), -+ onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) -+ }, t("Reset")) -+ ), -+ tab === "bot" ? (0, import_react30.createElement)(CharacterGrid, { -+ look, -+ hasPhoto, -+ onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), -+ onColor: (color) => run(actions.updateBot(bot.id, { color })) -+ }) : null, -+ tab === "upload" ? (0, import_react30.createElement)( -+ "div", -+ { -+ className: "bt-dropzone", -+ "data-over": over || void 0, -+ onDragOver: (event) => { -+ event.preventDefault(); -+ setOver(true); -+ }, -+ onDragLeave: () => setOver(false), -+ onDrop: (event) => { -+ event.preventDefault(); -+ setOver(false); -+ upload(event.dataTransfer.files?.[0]); -+ } -+ }, -+ hasPhoto ? (0, import_react30.createElement)("img", { src: look.image, alt: "" }) : null, -+ (0, import_react30.createElement)("div", null, t("Drag, drop, or paste an image")), -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-actions", style: { justifyContent: "center" } }, -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -+ hasPhoto ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null -+ ), -+ (0, import_react30.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ const file = event.target.files?.[0]; -+ event.target.value = ""; -+ upload(file); -+ } }) -+ ) : null - ); - } -- --// src/client/voice.js --var import_react35 = require("react"); --var VOICE_LABELS = { -- listening: "Listening", -- thinking: "Thinking", -- speaking: "Speaking", -- paused: "Paused", -- setup: "Voice input needs its speech model", -- unsupported: "Voice chat is not available in this browser" --}; --var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; --function useMicLevel(enabled) { -- const [level, setLevel] = (0, import_react35.useState)(0); -- (0, import_react35.useEffect)(() => { -- if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; -- let stream = null; -- let audio = null; -- let frame = 0; -- let stopped = false; -- void navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { -- if (stopped) { -- media.getTracks().forEach((track) => track.stop()); -- return; -- } -- stream = media; -- audio = new AudioContext(); -- const analyser = audio.createAnalyser(); -- analyser.fftSize = 256; -- audio.createMediaStreamSource(media).connect(analyser); -- const data = new Uint8Array(analyser.frequencyBinCount); -- let smooth = 0; -- const tick = () => { -- analyser.getByteTimeDomainData(data); -- let sum = 0; -- for (const value of data) sum += ((value - 128) / 128) ** 2; -- smooth = smooth * 0.8 + Math.min(1, Math.sqrt(sum / data.length) * 4) * 0.2; -- setLevel(Math.round(smooth * 100) / 100); -- frame = requestAnimationFrame(tick); -- }; -- tick(); -- }).catch(() => { -- }); -- return () => { -- stopped = true; -- cancelAnimationFrame(frame); -- stream?.getTracks().forEach((track) => track.stop()); -- void audio?.close().catch(() => { -- }); -- setLevel(0); -- }; -- }, [enabled]); -- return level; -+function MemoryCard({ bot, actions }) { -+ const count = useMemoryCount(bot.id, actions); -+ const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -+ return (0, import_react30.createElement)( -+ Card, -+ { title: t("Memory") }, -+ (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, label)), -+ (0, import_react30.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react30.createElement)(ChevronRightIcon)) -+ ) -+ ); - } --function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }) { -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const name = bot?.name ?? room2?.name ?? t("Bot"); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const lastReply = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; -- return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; -- }); -- const [speech] = (0, import_react35.useState)(() => actions.speech()); -- const [phase, setPhase] = (0, import_react35.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); -- const [catalog, setCatalog] = (0, import_react35.useState)(null); -- const [heard, setHeard] = (0, import_react35.useState)(""); -- const [said, setSaid] = (0, import_react35.useState)(""); -- const [note, setNote] = (0, import_react35.useState)(""); -- const [serviceLevel, setServiceLevel] = (0, import_react35.useState)(0); -- const [cycle, setCycle] = (0, import_react35.useState)(0); -- const waiting = (0, import_react35.useRef)(null); -- const reply = (0, import_react35.useRef)(lastReply); -- reply.current = lastReply; -- const turn = (0, import_react35.useRef)(0); -- const paneLeft = usePaneLeft(); -- const close = () => { -- turn.current += 1; -- window.speechSynthesis?.cancel(); -- actions.closeVoice(); -- }; -- useEscape(close); -- const submit = (text2) => { -- setHeard(text2); -- setPhase("thinking"); -- waiting.current = reply.current.split("|")[0]; -- void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); -- }; -- const dictation = useDictation((text2, final) => { -- setHeard(text2); -- if (!final) return; -- dictation.stop(); -- submit(text2); -- }); -- (0, import_react35.useEffect)(() => { -- if (!note) return void 0; -- const timer = setTimeout(() => setNote(""), 2600); -- return () => clearTimeout(timer); -- }, [note]); -- (0, import_react35.useEffect)(() => { -- if (!speech) return void 0; -- let live = true; -- speechCatalog(speech).then(({ catalog: next, usable }) => { -- if (!live) return; -- setCatalog(next); -- setPhase(usable ? "listening" : "setup"); -- }, () => { -- if (live) setPhase("setup"); -- }); -- return () => { -- live = false; -- }; -- }, [speech]); -- const listen = phase === "listening"; -- (0, import_react35.useEffect)(() => { -- if (speech || !listen || dictation.listening) return void 0; -- const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { -- } }), 250); -+function BotSettingsForm({ bot, roster, actions, run }) { -+ const [editing, setEditing] = (0, import_react30.useState)(false); -+ const [confirming, setConfirming] = (0, import_react30.useState)(false); -+ const closeEditor = (0, import_react30.useCallback)(() => setEditing(false), []); -+ (0, import_react30.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(false), 4e3); - return () => clearTimeout(timer); -- }, [listen, dictation.listening]); -- (0, import_react35.useEffect)(() => { -- if (phase !== "listening") dictation.stop(); -- }, [phase]); -- (0, import_react35.useEffect)(() => { -- if (!speech || !catalog || !listen) return void 0; -- const own = turn.current; -- let capture = null; -- let frame = 0; -- let stopped = false; -- let smooth = 0; -- const detect = turnDetector(); -- const limit = Math.max(1, catalog.maxDurationSeconds - 1) * 1e3; -- const finish = async (current) => { -- setPhase("transcribing"); -- try { -- const wave = await current.finish(catalog.maxDurationSeconds); -- const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; -- if (turn.current !== own) return; -- if (text2) submit(text2); -- else setPhase("listening"); -- } catch (error) { -- if (turn.current !== own) return; -- setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that")); -- setPhase("listening"); -- } -- }; -- openCapture().then((opened) => { -- if (stopped) { -- opened.cancel(); -- return; -- } -- capture = opened; -- const began = performance.now(); -- const tick = () => { -- const now = performance.now(); -- const level2 = capture.level(); -- smooth = smooth * 0.8 + level2 * 0.2; -- setServiceLevel(Math.round(smooth * 100) / 100); -- const state = detect(level2, now); -- if (state === "done" || state === "speech" && now - began >= limit) { -- const current = capture; -- capture = null; -- setServiceLevel(0); -- void finish(current); -- return; -- } -- if (state === "waiting" && now - began >= limit) { -- setCycle((value) => value + 1); -- return; -- } -- frame = requestAnimationFrame(tick); -- }; -- tick(); -- }, (error) => { -- if (stopped) return; -- setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that")); -- setPhase("paused"); -- }); -- return () => { -- stopped = true; -- cancelAnimationFrame(frame); -- capture?.cancel(); -- setServiceLevel(0); -- }; -- }, [speech, catalog, listen, cycle]); -- (0, import_react35.useEffect)(() => { -- if (phase !== "thinking" || running || waiting.current === null) return; -- const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; -- if (count === waiting.current || !text2.trim()) return; -- waiting.current = null; -- const spoken = speechOf(text2, botNames(roster)); -- setSaid(spoken); -- setHeard(""); -- if (!window.speechSynthesis || !spoken) { -- setPhase("listening"); -- return; -- } -- const utterance = new SpeechSynthesisUtterance(spoken); -- utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US"; -- utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current); -- utterance.onerror = utterance.onend; -- setPhase("speaking"); -- window.speechSynthesis.cancel(); -- window.speechSynthesis.speak(utterance); -- }, [phase, running, lastReply]); -- (0, import_react35.useEffect)(() => () => window.speechSynthesis?.cancel(), []); -- const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); -- const level = speech ? serviceLevel : micLevel; -- const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -- const blocked = phase === "unsupported" || phase === "setup" || phase === "starting"; -- const togglePause = () => { -- if (phase === "paused") { -- setPhase("listening"); -- return; -- } -- turn.current += 1; -- window.speechSynthesis?.cancel(); -- setPhase("paused"); -+ }, [confirming]); -+ if (bot.remote) return null; -+ const isMainBot = isMainOf(roster, bot.id); -+ const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; -+ const isTile = roster.mainBotId === bot.id; -+ const save = (patch) => run(actions.updateBot(bot.id, patch)); -+ const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id))); -+ const remove = () => { -+ if (confirming) void run(actions.deleteBot(bot.id)); -+ else setConfirming(true); - }; -- const openSetup = phase === "setup" ? actions.voiceSetup() : null; -- const caption = phase === "speaking" ? said : heard; -- const shownNote = note || dictation.note; -- return (0, import_react35.createElement)( -+ (0, import_react30.useEffect)(() => { -+ void actions.refreshModels?.(); -+ }, [bot.id]); -+ return (0, import_react30.createElement)( - "div", -- { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, -- (0, import_react35.createElement)( -+ { className: "bt-bs" }, -+ (0, import_react30.createElement)( - "div", -- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, -- (0, import_react35.createElement)( -+ { className: "bt-bs-hero" }, -+ (0, import_react30.createElement)( - "div", -- { className: "bt-voice-orb", style: { "--bt-level": level } }, -- (0, import_react35.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), -- (0, import_react35.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), -- bot ? (0, import_react35.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null -+ { className: "bt-avatar-anchor" }, -+ (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -+ (0, import_react30.createElement)(BotMark, { bot, size: 64, live: true }), -+ (0, import_react30.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react30.createElement)(PencilIcon)) -+ ), -+ editing ? (0, import_react30.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null - ), -- (0, import_react35.createElement)("div", { className: "bt-voice-name" }, name), -- (0, import_react35.createElement)( -+ (0, import_react30.createElement)( - "div", -- { className: "bt-voice-status", role: "status" }, -- phase === "thinking" ? (0, import_react35.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react35.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" -+ { className: "bt-bs-who" }, -+ (0, import_react30.createElement)("span", { className: "bt-bs-name" }, bot.name), -+ (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) -+ ) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Profile") }, -+ (0, import_react30.createElement)(Row, { label: t("Name") }, (0, import_react30.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -+ (0, import_react30.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react30.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Model") }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -+ (0, import_react30.createElement)(ModelMenu, { bot, roster, actions, run }) - ), -- (0, import_react35.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), -- openSetup ? (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { -- close(); -- openSetup(); -- } }, t("Set up voice input")) : null, -- shownNote ? (0, import_react35.createElement)("div", { className: "bt-voice-note" }, shownNote) : null -+ bot.model ? (0, import_react30.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null - ), -- (0, import_react35.createElement)( -- "div", -- { className: "bt-voice-bar" }, -- (0, import_react35.createElement)("button", { -- type: "button", -- className: "bt-voice-btn", -- "aria-pressed": phase === "paused", -- disabled: blocked, -- "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), -- onClick: togglePause -- }, (0, import_react35.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react35.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), -- (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react35.createElement)(CloseIcon)) -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Working folder") }, -+ (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -+ (0, import_react30.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) -+ ) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Instructions") }, -+ (0, import_react30.createElement)( -+ Row, -+ { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -+ (0, import_react30.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) -+ ) -+ ), -+ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). -+ (0, import_react30.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react30.createElement)(Card, { title: t("Sharing") }, children) }), -+ (0, import_react30.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -+ (0, import_react30.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), -+ roster.memory === false ? null : (0, import_react30.createElement)(MemoryCard, { bot, actions }), -+ (0, import_react30.createElement)(BotSchedules, { bot, roster, actions }), -+ (0, import_react30.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("Main Bot") }, -+ onlyMain ? (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -+ ), -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -+ ) : (0, import_react30.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) -+ ), -+ // The Main Bot tile is always first and always shown, like in the context menu. -+ isTile ? null : (0, import_react30.createElement)( -+ Card, -+ { title: t("Sidebar") }, -+ (0, import_react30.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -+ isMainBot ? null : (0, import_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -+ ), -+ (0, import_react30.createElement)( -+ Card, -+ { title: t("More") }, -+ (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -+ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) -+ ), -+ isMainBot ? null : (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -+ (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -+ ), -+ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) -+ ) - ) - ); - } -+var BOT_SETTINGS_CSS = ` -+.bt-bs{display:flex;flex-direction:column;gap:18px} -+.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} -+.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} -+.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-bs .bt-avedit{left:0;transform:none} -+.bt-bs-row{min-height:48px} -+label.bt-bs-row{cursor:text} -+.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} -+/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to -+ break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test -+ connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under -+ a label is prose, so it wraps and is never cut: the row grows instead. */ -+.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} -+.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} -+.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} -+.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} -+${MODEL_TEST_CSS} -+.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} -+.bt-bs-input:hover{background:var(--bt-hover)} -+.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -+.bt-bs-input::placeholder{color:var(--bt-ink-3)} -+.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} -+.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} -+.bt-bs-area:hover{background:var(--bt-active)} -+.bt-bs-danger .bt-bs-label{color:#c21d2e} -+.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} -+.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} -+.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} -+.bt-switch[aria-checked=true]{background:var(--bt-accent)} -+.bt-switch:disabled{opacity:.45;cursor:default} -+.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} -+.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} -+.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} -+.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} -+.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} -+.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} -+@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} -+/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below -+ 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and -+ keeps its own words. Nothing is clipped to fit a width that is not there. */ -+@container bt-panel (max-width:300px){ -+ .bt-bs-row{flex-wrap:wrap;row-gap:6px} -+ .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} -+ .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} -+} -+`; - --// src/client/workflows.js --var import_react36 = require("react"); --var READ_MS = 8e3; --var POLL_MS6 = 2e3; --var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; --var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; --function workflowCallMissing(failure) { -- const code = failure && typeof failure === "object" ? String(failure.code ?? "") : ""; -- const message = typeof failure === "string" ? failure : String(failure?.message ?? ""); -- return code === "bot/not-found" || /Unknown FlyBot endpoint workflow\//i.test(message); -+// src/client/group-settings.js -+var import_react31 = require("react"); -+var ROOM_MODES = [ -+ ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], -+ ["everyone", "Everyone", "Every member answers in turn, admin first."] -+]; -+var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone"; -+function faceSeatBot(seat, roster) { -+ if (!seat) return void 0; -+ const record = seat.record && typeof seat.record === "object" ? seat.record : {}; -+ const extra = {}; -+ if (typeof record.color === "string" && record.color) extra.color = record.color; -+ if (record.avatar && typeof record.avatar === "object") extra.avatar = record.avatar; -+ if (seat.local && !seat.local.remote) return Object.keys(extra).length ? { ...seat.local, ...extra } : seat.local; -+ const handle = typeof record.handle === "string" && record.handle.trim() ? record.handle : typeof seat.key === "string" && seat.key.startsWith("@") ? seat.key : ""; -+ return lookBotForRemote({ -+ handle, -+ displayName: record.displayName || seat.bot, -+ name: seat.bot, -+ label: record.label, -+ id: record.id || seat.key, -+ ...extra -+ }, roster); - } --function workflowCallError(failure, fallback = "Could not read workflows") { -- if (workflowCallMissing(failure)) return WORKFLOW_PLUGIN_HINT; -- if (typeof failure === "string" && failure.trim()) return failure.trim(); -- const message = typeof failure?.message === "string" ? failure.message.trim() : ""; -- return message || fallback; -+function faceMemberLook(bot) { -+ if (!bot) return void 0; -+ return bot.remote ? lookAvoidingLocals(bot) : lookOf(bot); - } --function settleWorkflowRead(value, fallback = "Could not read workflows") { -- if (!value || value.ok === false || !Array.isArray(value.workflows)) { -- const raw = typeof value?.error === "string" && value.error.trim() ? value.error : ""; -- const error = workflowCallMissing(value) || workflowCallMissing(raw) ? WORKFLOW_PLUGIN_HINT : raw || fallback; -- return { phase: "error", error, pack: null }; -+function faceMarks(person, roster, size) { -+ const marks = []; -+ for (const seat of person?.seats ?? []) { -+ const bot = faceSeatBot(seat, roster); -+ if (!bot) continue; -+ marks.push((0, import_react31.createElement)(BotMark, { key: seat.key, bot, size, live: false })); - } -- const templates = Array.isArray(value.templates) ? value.templates : []; -- const workflows = value.workflows; -- const pack = { ...value, templates, workflows }; -- if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack }; -- return { phase: "ready", error: "", pack }; -+ if (marks.length === 0) return [(0, import_react31.createElement)(PersonMark, { key: "person", size, name: person?.name })]; -+ return marks; - } --function readWorkflows(call4, { timeoutMs = READ_MS } = {}) { -- return new Promise((resolve) => { -- let settled = false; -- const finish = (value) => { -- if (settled) return; -- settled = true; -- clearTimeout(timer); -- resolve(value); -- }; -- const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs); -- Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null })); -- }); --} --function moveNode(nodes, id, x, y) { -- return nodes.map((node) => node.id === id ? { ...node, x, y } : node); --} --function connectNodes(edges, from, to) { -- if (!from || !to || from === to) return edges; -- if (edges.some((edge) => edge.from === from && edge.to === to)) return edges; -- return edges.concat([{ from, to }]); -+function faceRoomCount(room2, roster) { -+ const people2 = facePeople(room2, roster).length; -+ const seats = faceSeats(room2, roster).length; -+ const persons = people2 === 1 ? t("1 person") : t("{count} people", { count: people2 }); -+ const bots = seats === 1 ? t("1 Bot") : t("{count} Bots", { count: seats }); -+ return `${persons} · ${bots}`; - } --function dropBot(nodes, bot, x, y) { -- const ids = new Set(nodes.map((node) => node.id)); -- let n = nodes.length + 1; -- while (ids.has(`n${n}`)) n += 1; -- return nodes.concat([{ id: `n${n}`, name: bot.name, botId: bot.id, role: bot.role ?? "", x, y }]); -+var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLowerCase(); -+var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); -+function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { -+ const bots = person.seats.flatMap((seat, index) => { -+ const name = seat.local && onOpenBot ? (0, import_react31.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react31.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); -+ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react31.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; -+ return index === 0 ? marked : [(0, import_react31.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; -+ }); -+ return (0, import_react31.createElement)( -+ "div", -+ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-gs-who bt-face-person" }, -+ (0, import_react31.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-face-person-text" }, -+ (0, import_react31.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), -+ (0, import_react31.createElement)("span", { className: "bt-face-bots" }, bots) -+ ) -+ ) -+ ); - } --function removeNode(nodes, edges, id) { -- return { -- nodes: nodes.filter((node) => node.id !== id), -- edges: edges.filter((edge) => edge.from !== id && edge.to !== id) -+function FaceLead({ room: room2, roster, pick: pick2 }) { -+ const lead = room2.face?.lead; -+ const [open, setOpen] = (0, import_react31.useState)(false); -+ const [busy, setBusy] = (0, import_react31.useState)(false); -+ const [error, setError] = (0, import_react31.useState)(""); -+ const seats = faceSeats(room2, roster); -+ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); -+ if (!current) return null; -+ const canPick = lead.canPick === true && typeof pick2 === "function"; -+ const choose = async (handle) => { -+ setBusy(true); -+ setError(""); -+ try { -+ await pick2(handle); -+ setOpen(false); -+ } catch (err) { -+ setError(err?.message || t("Could not change the room lead.")); -+ } finally { -+ setBusy(false); -+ } - }; -+ return (0, import_react31.createElement)( -+ "div", -+ { className: "bt-face-lead", "data-flybot": "face-lead" }, -+ (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Room lead")), -+ (0, import_react31.createElement)( -+ "div", -+ { className: "bt-set-row" }, -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react31.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), -+ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) -+ ), -+ canPick ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null -+ ), -+ open && canPick ? seats.map((seat) => (0, import_react31.createElement)( -+ "div", -+ { key: seat.key, className: "bt-set-row" }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), -+ seat === current ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) -+ )) : null, -+ open && canPick && lead.picked ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, -+ canPick || !lead.supported ? null : (0, import_react31.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), -+ error ? (0, import_react31.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null -+ ); - } --function chainOf(item) { -- if (typeof item?.chain === "string" && item.chain) return item.chain; -- if (!Array.isArray(item?.layers) || !Array.isArray(item?.nodes)) return ""; -- const name = (id) => item.nodes.find((node) => node.id === id)?.name ?? id; -- return item.layers.map((layer) => layer.map(name).join(" ‖ ")).join(" → "); --} --function openedGraph(pack, opened) { -- if (!pack || !opened) return null; -- if (opened.source === "own") return (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; -- return (pack.templates ?? []).find((item) => item.id === opened.id) ?? (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; --} --function statusLabel2(status) { -- if (status === "running") return t("Running"); -- if (status === "done") return t("Done"); -- if (status === "failed") return t("Failed"); -- if (status === "idle") return t("Idle"); -- if (status === "skipped") return t("Skipped"); -- return t("Queued"); --} --function hitNode(event) { -- const direct = event.target?.dataset?.node ? event.target : null; -- const nested = event.target?.closest?.("[data-node]"); -- return (nested ?? direct)?.dataset?.node || ""; --} --var WIRE_SPAN = 480; --var NODE_WIRE_OUT = 56; --var CARD_HALF = 84; --var CARD_H = 72; --var FRAME_SIDE = 108; --var FRAME_LABEL = 18; --var FRAME_FOOT = 10; --var MIN_FIT = 0.5; --function wirePath(from, to) { -- const x1 = from.x + WIRE_SPAN; -- const y1 = from.y + NODE_WIRE_OUT; -- const x2 = to.x + WIRE_SPAN; -- const y2 = to.y; -- const mid = (y1 + y2) / 2; -- return `M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`; -+function seatedCount(room2) { -+ const live = room2?.presentedParticipants ?? room2?.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length; -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length; -+ return humans + remotes + (room2?.members?.length ?? 0); - } --function fitScale(nodes, width) { -- const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -- if (!(width > 0) || reach === 0) return 1; -- return Math.min(1, Math.max(MIN_FIT, width / 2 / reach)); -+function roomSubtitle(room2, roster) { -+ if (room2.origin === "face-to-face") return faceRoomCount(room2, roster); -+ const admin = roster.byId[room2.admin]; -+ const count = seatedCount(room2); -+ return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); - } --function fitLayout(nodes, width) { -- const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -- const scale = fitScale(nodes, width); -- const spare = width > 0 && reach > 0 ? reach * scale - width / 2 : 0; -- const pad2 = spare > 1e-6 ? Math.ceil(spare) : 0; -- const centre = width > 0 ? pad2 + width / 2 : 0; -- return { -- reach, -- scale, -- pad: pad2, -- left: centre - reach * scale, -- right: centre + reach * scale, -- scrollWidth: Math.max(width + pad2, centre + reach * scale) -+function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { -+ const [adding, setAdding] = (0, import_react31.useState)(false); -+ const [confirming, setConfirming] = (0, import_react31.useState)(false); -+ (0, import_react31.useEffect)(() => { -+ if (!confirming) return void 0; -+ const timer = setTimeout(() => setConfirming(false), 4e3); -+ return () => clearTimeout(timer); -+ }, [confirming]); -+ const live = room2.presentedParticipants ?? room2.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); -+ const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -+ const face = room2.origin === "face-to-face"; -+ const people2 = face ? facePeople(room2, roster) : []; -+ const shared2 = humans.length > 1; -+ const chiefs = new Set((roster.humans ?? []).map((person) => person.designatedChiefId).filter(Boolean)); -+ const outside = roster.bots.filter((bot) => { -+ if (bot.remote) return false; -+ if (room2.members.includes(bot.id)) return false; -+ if (shared2) return chiefs.has(bot.id); -+ return true; -+ }); -+ const outsidePeople = (roster.humans ?? []).filter((person) => !humans.some((p) => p.id === person.id)); -+ const outsideRemotes = (roster.remoteBots ?? []).filter((bot) => !remotes.some((p) => p.handle === bot.handle)); -+ const admin = roster.byId[room2.admin]; -+ const mode = shownMode(room2, roster); -+ const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? ""; -+ const update = (patch) => run(actions.updateRoom(room2.id, patch)); -+ const openBot = (id) => { -+ actions.closeSettings(); -+ actions.openSession(id); - }; --} --function measureCanvasWidth(el) { -- const width = el?.clientWidth; -- return Number.isFinite(width) && width > 0 ? width : 0; --} --function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) { -- const gesture = (0, import_react36.useRef)(null); -- const canvasRef = (0, import_react36.useRef)(null); -- const [measuredWidth, setCanvasWidth] = (0, import_react36.useState)(0); -- const nodes = graph.nodes ?? []; -- const edges = graph.edges ?? []; -- const status = graph.run?.nodeStatus ?? null; -- const locals = (roster?.bots ?? []).filter((bot) => bot && bot.remote !== true && !nodes.some((node) => node.botId === bot.id)); -- const height = Math.max(420, ...nodes.map((node) => (node.y ?? 0) + 150), 0); -- const canvasWidth = measuredWidth || knownWidth; -- const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth); -- const drawnHeight = Math.round(height * scale); -- (0, import_react36.useEffect)(() => { -- const el = canvasRef.current; -- const measure = () => { -- const width = measureCanvasWidth(el); -- if (width > 0 && Math.abs(width - canvasWidth) > 0.5) setCanvasWidth(width); -- }; -- measure(); -- if (typeof ResizeObserver !== "function" || !el) return void 0; -- const observer = new ResizeObserver(measure); -- observer.observe(el); -- return () => observer.disconnect(); -- }, [canvasWidth]); -- const end = (event) => { -- const current = gesture.current; -- gesture.current = null; -- if (!current || !editing) return; -- if (current.kind === "move") { -- const x = Math.round(current.x + (event.clientX - current.ox) / scale); -- const y = Math.round(current.y + (event.clientY - current.oy) / scale); -- onCommit?.({ nodes: moveNode(nodes, current.id, x, y), edges }); -- } else if (current.kind === "wire") { -- const next = connectNodes(edges, current.from, hitNode(event)); -- if (next !== edges) onCommit?.({ nodes, edges: next }); -- } else if (current.kind === "add" && event.target?.dataset?.plane) { -- const x = Number.isFinite(event.offsetX) ? Math.round(event.offsetX / scale) : 24; -- const y = Number.isFinite(event.offsetY) ? Math.round(event.offsetY / scale) : 24; -- onCommit?.({ nodes: dropBot(nodes, current.bot, x, y), edges }); -+ const remove = () => { -+ if (!confirming) { -+ setConfirming(true); -+ return; - } -+ void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { -+ if (done) onDeleted?.(); -+ }); - }; -- return (0, import_react36.createElement)( -+ return (0, import_react31.createElement)( - "div", -- { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end }, -- editing ? (0, import_react36.createElement)( -+ { className: "bt-bs" }, -+ (0, import_react31.createElement)( - "div", -- { className: "bt-wf-palette" }, -- (0, import_react36.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), -- locals.map((bot) => (0, import_react36.createElement)("button", { -- key: bot.id, -- type: "button", -- "data-bot": bot.id, -- onPointerDown: (event) => { -- event.preventDefault?.(); -- gesture.current = { kind: "add", bot }; -- } -- }, bot.name)) -- ) : null, -- // A node's system prompt is the run Bot's prompt (plugins/workflow -- // nodeConfig). The sidebar panel has this field, so the Settings editor -- // writes the same graph instead of only drawing it (#1393). -- editing ? (0, import_react36.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react36.createElement)( -- "label", -- { key: node.id, className: "bt-wf-prompt" }, -- (0, import_react36.createElement)("span", null, `${node.name} · ${t("System prompt")}`), -- (0, import_react36.createElement)("textarea", { -- "data-prompt": node.id, -- rows: 3, -- defaultValue: node.prompt || "", -- onPointerDown: (event) => event.stopPropagation?.(), -- onBlur: (event) => { -- const prompt = event.target?.value ?? ""; -- if (prompt === (node.prompt || "")) return; -- onCommit?.({ nodes: nodes.map((item) => item.id === node.id ? { ...item, prompt } : item), edges }); -- } -- }) -- ))) : null, -- (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, -- (0, import_react36.createElement)( -+ { className: "bt-bs-hero" }, -+ (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ (0, import_react31.createElement)( - "div", -- { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, -- (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, -- (0, import_react36.createElement)( -- "svg", -- { -- className: "bt-wf-wires", -- "aria-hidden": true, -- width: WIRE_SPAN * 2, -- height, -- viewBox: `0 0 ${WIRE_SPAN * 2} ${height}` -- }, -- (0, import_react36.createElement)( -- "defs", -- null, -- (0, import_react36.createElement)("marker", { -- id: "bt-wf-arrow", -- viewBox: "0 0 10 10", -- refX: "8", -- refY: "5", -- markerWidth: "6", -- markerHeight: "6", -- orient: "auto" -- }, (0, import_react36.createElement)("path", { d: "M0 0 10 5 0 10z" })) -- ), -- edges.map((edge) => { -- const from = nodes.find((node) => node.id === edge.from); -- const to = nodes.find((node) => node.id === edge.to); -- if (!from || !to) return null; -- return (0, import_react36.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" }); -- }) -- ), -- (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => { -- const members = layer.map((id) => nodes.find((node) => node.id === id)).filter(Boolean); -- if (members.length < 2) return null; -- const left = Math.min(...members.map((node) => node.x ?? 0)) - FRAME_SIDE; -- const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE; -- const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL; -- const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT; -- return (0, import_react36.createElement)("div", { -- key: layer.join("+"), -- className: "bt-wf-par", -- style: { left, top, width: right - left, height: bottom - top } -- }, (0, import_react36.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join"))); -- }), -- nodes.map((node) => { -- const state = status ? status[node.id] ?? "queued" : "idle"; -- return (0, import_react36.createElement)( -- "div", -- { -- key: node.id, -- "data-node": node.id, -- className: `bt-wf-node ${state}${editing ? " editing" : ""}`, -- style: { left: (node.x ?? 0) - CARD_HALF, top: node.y ?? 0 }, -- onPointerDown: (event) => { -- if (!editing || event.target?.dataset?.port || event.target?.dataset?.remove) return; -- gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 }; -- } -- }, -- editing ? (0, import_react36.createElement)("button", { -- type: "button", -- className: "bt-wf-port", -- "data-port": "out", -- "aria-label": node.name, -- onPointerDown: (event) => { -- event.preventDefault?.(); -- event.stopPropagation?.(); -- gesture.current = { kind: "wire", from: node.id }; -- } -- }) : null, -- editing ? (0, import_react36.createElement)("button", { -- type: "button", -- className: "bt-wf-remove", -- "data-remove": node.id, -- "aria-label": t("Remove"), -- onClick: (event) => { -- event.stopPropagation?.(); -- const next = removeNode(nodes, edges, node.id); -- onCommit?.(next); -- } -- }, "×") : null, -- (0, import_react36.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)), -- (0, import_react36.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)), -- (0, import_react36.createElement)( -- "span", -- { className: "bt-wf-copy" }, -- (0, import_react36.createElement)("span", { className: "bt-wf-name" }, node.name), -- (0, import_react36.createElement)("span", { className: "bt-wf-role" }, node.role ?? "") -- ) -- ); -- }) -- ) -+ { className: "bt-bs-who" }, -+ (0, import_react31.createElement)("span", { className: "bt-bs-name" }, room2.name), -+ (0, import_react31.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) - ) -- ) -- ); --} --function Row2({ item, source, onOpen, onRename, onDelete }) { -- return (0, import_react36.createElement)( -- "button", -- { -- type: "button", -- className: "bt-set-row", -- "data-workflow": item.id, -- "data-source": source, -- onClick: () => onOpen?.(item) -- }, -- (0, import_react36.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react36.createElement)("span", null, item.name), -- (0, import_react36.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") - ), -- source === "own" ? (0, import_react36.createElement)( -- "span", -- { className: "bt-set-actions" }, -- (0, import_react36.createElement)("button", { -- type: "button", -- className: "bt-set-action", -- "aria-label": t("Rename"), -- title: t("Rename"), -- onClick: (event) => { -- event.stopPropagation(); -- onRename?.(item); -- } -- }, "✎"), -- (0, import_react36.createElement)("button", { -- type: "button", -- className: "bt-set-action", -- "aria-label": t("Delete"), -- title: t("Delete"), -- onClick: (event) => { -- event.stopPropagation(); -- onDelete?.(item); -- } -- }, "×") -- ) : null, -- (0, import_react36.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›") -- ); --} --function alertCopy(error) { -- if (error === WORKFLOW_PLUGIN_HINT) return t("The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."); -- return error || t("Could not read workflows"); --} --function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, canvasWidth = 0, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) { -- const graph = phase === "ready" ? openedGraph(pack, opened) : null; -- const templates = pack?.templates ?? []; -- const templateIds = new Set(templates.map((item) => item.id)); -- const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); -- const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; -- return (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-page" }, -- (0, import_react36.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_react36.createElement)( -- "div", -- { className: "bt-error", role: "alert" }, -- alertCopy(error), -- phase === "error" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null -- ) : null, -- phase === "loading" ? (0, import_react36.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null, -- phase === "empty" ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null, -- phase === "ready" && !graph ? (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-lists" }, -- (0, import_react36.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react36.createElement)("h3", null, t("Factory presets")), -- (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")), -- (0, import_react36.createElement)( -- "div", -- { className: "bt-set-card" }, -- templates.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => { -- const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id); -- const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item; -- return (0, import_react36.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); -- }) -- ) -+ (0, import_react31.createElement)( -+ Card, -+ { title: t("Profile") }, -+ (0, import_react31.createElement)( -+ Row, -+ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -+ (0, import_react31.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) - ), -- (0, import_react36.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react36.createElement)("h3", null, t("Yours")), -- (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), -- (0, import_react36.createElement)( -- "div", -- { className: "bt-set-card" }, -- yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) -- ) -+ (0, import_react31.createElement)( -+ Row, -+ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -+ (0, import_react31.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) - ) -- ) : null, -- graph ? (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-editor" }, -- (0, import_react36.createElement)( -- "div", -- { className: "bt-wf-actions" }, -- (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), -- showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, -- opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null -- ), -- (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name), -- graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, -- (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, { -- graph, -- roster, -- onCommit, -- width: canvasWidth, -- editing: opened?.source === "own" && editing === true -- }) -- ) : null -- ); --} --function RenameModal({ workflow, onClose, onConfirm }) { -- const [name, setName] = (0, import_react36.useState)(workflow?.name ?? ""); -- return (0, import_react36.createElement)( -- "div", -- { className: "bt-modal-mask", onClick: (event) => { -- if (event.target === event.currentTarget) onClose?.(); -- } }, -- (0, import_react36.createElement)( -- "div", -- { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, -- (0, import_react36.createElement)("h3", null, t("Rename workflow")), -- (0, import_react36.createElement)( -- "label", -- { className: "bt-field" }, -- t("Name"), -- (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) -- ), -- (0, import_react36.createElement)( -+ ), -+ (0, import_react31.createElement)( -+ Card, -+ { title: t("Who answers") }, -+ (0, import_react31.createElement)( - "div", -- { className: "bt-modal-actions" }, -- (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -- (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react31.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react31.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_react31.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) - ) -- ) -- ); --} --function DeleteModal({ workflow, onClose, onConfirm }) { -- return (0, import_react36.createElement)( -- "div", -- { className: "bt-modal-mask", onClick: (event) => { -- if (event.target === event.currentTarget) onClose?.(); -- } }, -- (0, import_react36.createElement)( -- "div", -- { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, -- (0, import_react36.createElement)("h3", null, t("Delete workflow")), -- (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), -- (0, import_react36.createElement)( -+ ), -+ (0, import_react31.createElement)( -+ Card, -+ { title: t("Working folder") }, -+ (0, import_react31.createElement)( - "div", -- { className: "bt-modal-actions" }, -- (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -- (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) -+ { className: "bt-set-row bt-bs-row" }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -+ (0, import_react31.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) - ) -- ) -- ); --} --function WorkflowsPage({ call: call4, roster }) { -- const [phase, setPhase] = (0, import_react36.useState)("loading"); -- const [pack, setPack] = (0, import_react36.useState)(null); -- const [error, setError] = (0, import_react36.useState)(""); -- const [opened, setOpened] = (0, import_react36.useState)(null); -- const [editing, setEditing] = (0, import_react36.useState)(false); -- const [renaming, setRenaming] = (0, import_react36.useState)(null); -- const [deleting, setDeleting] = (0, import_react36.useState)(null); -- const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows")); -- const apply2 = (settled) => { -- setPhase(settled.phase); -- setPack(settled.pack); -- setError(settled.error); -- }; -- (0, import_react36.useEffect)(() => { -- let gone = false; -- readWorkflows(invoke).then((settled) => { -- if (!gone) apply2(settled); -- }); -- return () => { -- gone = true; -- }; -- }, []); -- const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); -- (0, import_react36.useEffect)(() => { -- if (!live || editing) return void 0; -- let gone = false; -- const timer = setInterval(() => { -- readWorkflows(invoke).then((settled) => { -- if (!gone) apply2(settled); -- }); -- }, POLL_MS6); -- return () => { -- gone = true; -- clearInterval(timer); -- }; -- }, [live, editing]); -- const retry = () => { -- setPhase("loading"); -- setError(""); -- setOpened(null); -- readWorkflows(invoke).then(apply2); -- }; -- const open = (item, source) => { -- if (source === "factory") { -- const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); -- setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); -- setEditing(false); -- return; -- } -- setOpened({ source: "own", id: item.id }); -- setEditing(false); -- }; -- const useTemplate = (id) => { -- const template = typeof id === "string" ? id : ""; -- if (!template) return; -- Promise.resolve(invoke("workflow/create", { template })).then((value) => { -- const settled = settleWorkflowRead(value); -- if (settled.phase === "error") { -- setError(settled.error); -- return; -- } -- const before = new Set((pack?.workflows ?? []).map((item) => item.id)); -- const made = settled.pack.workflows.find((item) => !before.has(item.id)); -- setPack(settled.pack); -- setPhase(settled.phase); -- setError(""); -- if (made) { -- setOpened({ source: "own", id: made.id }); -- setEditing(true); -- } -- }).catch((failure) => { -- setError(workflowCallError(failure, t("Could not copy this workflow"))); -- }); -- }; -- const rename = (item) => { -- setRenaming(item); -- }; -- const confirmRename = (workflowId, name) => { -- if (!name.trim()) return; -- Promise.resolve(invoke("workflow/rename", { workflowId, name: name.trim() })).then((value) => { -- const settled = settleWorkflowRead(value); -- if (settled.phase === "error") { -- setError(settled.error); -- return; -- } -- setPack(settled.pack); -- setPhase("ready"); -- setError(""); -- setRenaming(null); -- }).catch((failure) => { -- setError(workflowCallError(failure, t("Could not rename this workflow"))); -- }); -- }; -- const deleteWorkflow = (item) => { -- setDeleting(item); -- }; -- const confirmDelete = (workflowId) => { -- Promise.resolve(invoke("workflow/delete", { workflowId })).then((value) => { -- const settled = settleWorkflowRead(value); -- if (settled.phase === "error") { -- setError(settled.error); -- return; -- } -- setPack(settled.pack); -- setPhase(settled.phase); -- setError(""); -- setDeleting(null); -- if (opened?.id === workflowId) setOpened(null); -- }).catch((failure) => { -- setError(workflowCallError(failure, t("Could not delete this workflow"))); -- }); -+ ), -+ face ? (0, import_react31.createElement)( -+ Card, -+ { title: t("Members · {count}", { count: people2.length }) }, -+ people2.map((person) => (0, import_react31.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) -+ ) : null, -+ face ? null : (0, import_react31.createElement)( -+ Card, -+ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, -+ humans.map((person) => (0, import_react31.createElement)( -+ "div", -+ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-gs-who" }, -+ (0, import_react31.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), -+ (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Human")), -+ person.role === "admin" ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -+ ), -+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null -+ )), -+ members.map((member) => { -+ const leads = room2.admin === member.id; -+ const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; -+ const taskLead = room2.taskLead === member.id; -+ return (0, import_react31.createElement)( -+ "div", -+ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react31.createElement)( -+ "button", -+ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -+ (0, import_react31.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -+ (0, import_react31.createElement)("span", { className: "bt-row-title" }, member.name), -+ leads ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, -+ taskLead ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, -+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Chief")) : null -+ ), -+ leads ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -+ boardHere ? taskLead ? (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react31.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, -+ members.length > 2 ? (0, import_react31.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 -+ ); -+ }), -+ remotes.map((person) => (0, import_react31.createElement)( -+ "div", -+ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-gs-who" }, -+ (0, import_react31.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react31.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), -+ // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is -+ // labelled as such; a Bot with no Bot Social identity can never be written to at all. -+ room2.origin === "face-to-face" ? null : (0, import_react31.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) -+ ), -+ // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. -+ room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") -+ )), -+ room2.origin === "face-to-face" ? null : adding ? [ -+ ...outsidePeople.map((person) => (0, import_react31.createElement)( -+ "button", -+ { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -+ setAdding(false); -+ void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); -+ } }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), -+ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add person")) -+ )), -+ ...outside.map((bot) => (0, import_react31.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_react31.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, bot.name)), -+ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add")) -+ )), -+ ...outsideRemotes.map((bot) => (0, import_react31.createElement)( -+ "button", -+ { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -+ setAdding(false); -+ void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); -+ } }, -+ (0, import_react31.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), -+ (0, import_react31.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) -+ )), -+ (0, import_react31.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -+ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react31.createElement)(PlusIcon), t("Add member")) : null, -+ (0, import_react31.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react31.createElement)( -+ "span", -+ { className: "bt-set-hint" }, -+ // The host's two-Bot floor guards membership changes; this hint warns before one. -+ members.length + humans.length + remotes.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.")}${room2.origin === "face-to-face" || room2.origin === "inbound-share" ? "" : ` ${t("The task lead divides the board. When unset, the admin does.")}`}` -+ )), -+ // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after -+ // the note on who leads, so that note stays with the members it is about (#892). -+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react31.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) -+ ), -+ (0, import_react31.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), -+ (0, import_react31.createElement)( -+ Card, -+ { title: t("Sidebar") }, -+ (0, import_react31.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_react31.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_react31.createElement)( -+ Card, -+ { title: t("More") }, -+ (0, import_react31.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -+ (0, import_react31.createElement)( -+ "span", -+ { className: "bt-set-copy" }, -+ (0, import_react31.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -+ (0, import_react31.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_react31.createElement)("span", { className: "bt-chevron" }, (0, import_react31.createElement)(ChevronRightIcon)) -+ ) -+ ) -+ ); -+} -+function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -+ const [admin, setAdmin] = (0, import_react31.useState)(null); -+ const [members, setMembers] = (0, import_react31.useState)([]); -+ const [name, setName] = (0, import_react31.useState)(""); -+ const [busy, setBusy] = (0, import_react31.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 commit = (graph) => { -- if (!opened || opened.source !== "own") return; -- Promise.resolve(invoke("workflow/graph", { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges })).then((value) => { -- const settled = settleWorkflowRead(value); -- if (settled.phase === "error") { -- setError(settled.error); -- return; -- } -- setPack(settled.pack); -- setPhase("ready"); -- setError(""); -- }).catch((failure) => { -- setError(workflowCallError(failure, t("Could not save this workflow"))); -- }); -+ const create = async () => { -+ if (busy || admin === null || members.length === 0) return; -+ setBusy(true); -+ let room2; -+ const done = await run(actions.createRoom([admin, ...members], { admin, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -+ room2 = value; -+ })); -+ if (done && room2) onCreated(room2); -+ else setBusy(false); - }; -- return (0, import_react36.createElement)( -+ const chip = (bot, on, onClick, role) => (0, import_react31.createElement)("button", { -+ key: bot.id, -+ type: "button", -+ role, -+ className: "bt-ng-chip", -+ "aria-checked": on, -+ onClick -+ }, (0, import_react31.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react31.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react31.createElement)(AdminStar) : null); -+ return (0, import_react31.createElement)( - "div", -- null, -- (0, import_react36.createElement)(WorkflowsView, { -- phase, -- pack, -- error, -- opened, -- editing, -- roster, -- onOpen: open, -- onBack: () => { -- setOpened(null); -- setEditing(false); -- }, -- onEditing: setEditing, -- onRetry: retry, -- onUse: useTemplate, -- onCommit: commit, -- onRename: rename, -- onDelete: deleteWorkflow -- }), -- renaming ? (0, import_react36.createElement)(RenameModal, { -- workflow: renaming, -- onClose: () => setRenaming(null), -- onConfirm: (name) => confirmRename(renaming.id, name) -- }) : null, -- deleting ? (0, import_react36.createElement)(DeleteModal, { -- workflow: deleting, -- onClose: () => setDeleting(null), -- onConfirm: () => confirmDelete(deleting.id) -- }) : null -+ { className: "bt-set-card bt-ng" }, -+ (0, import_react31.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react31.createElement)(AdminStar), t("Choose the group admin"))), -+ (0, import_react31.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")) -+ ) -+ ), -+ admin === null ? null : (0, import_react31.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -+ (0, import_react31.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")) -+ ) -+ ), -+ admin === null || members.length === 0 ? null : (0, import_react31.createElement)( -+ "label", -+ { className: "bt-set-row bt-bs-row bt-ng-step" }, -+ (0, import_react31.createElement)("span", { className: "bt-set-copy" }, (0, import_react31.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react31.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -+ ), -+ (0, import_react31.createElement)( -+ "div", -+ { className: "bt-set-row bt-task-foot" }, -+ (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) -+ ) - ); - } -- --// src/client/usage-data.js --var HOUR = 0; --var OWNER = 1; --var MODEL = 2; --var KIND = 3; --var HOUR_MS = 36e5; --var DAY_MS = 864e5; --var RANGES = [ -- { id: "24h", unit: "hour", count: 24, stride: 6 }, -- { id: "7d", unit: "day", count: 7, stride: 1 }, -- { id: "30d", unit: "day", count: 30, stride: 5 }, -- { id: "12w", unit: "week", count: 12, stride: 2 }, -- { id: "12m", unit: "month", count: 12, stride: 1 } --]; --var hourMs = (key) => Date.UTC(Number(key.slice(0, 4)), Number(key.slice(5, 7)) - 1, Number(key.slice(8, 10)), Number(key.slice(11, 13)) || 0); --var dayKey = (ms) => new Date(ms).toISOString().slice(0, 10); --var hourKey = (ms) => new Date(ms).toISOString().slice(0, 13).replace("T", " "); --var mondayOf = (ms) => ms - (new Date(ms).getUTCDay() + 6) % 7 * DAY_MS; --function spanRange(from, to) { -- if (to < from) [from, to] = [to, from]; -- const days = Math.round((hourMs(to) - hourMs(from)) / DAY_MS) + 1; -- const unit = days <= 2 ? "hour" : days <= 62 ? "day" : days <= 182 ? "week" : "month"; -- const range = { id: "custom", unit, from, to, days }; -- const count = spanBuckets(range).length; -- const stride = unit === "hour" ? 6 : [1, 2, 3, 5, 7, 10, 14].find((step) => count / step <= 12) ?? Math.ceil(count / 12); -- return { ...range, count, stride }; --} --function spanBuckets(range) { -- const first = hourMs(range.from); -- const end = hourMs(range.to) + DAY_MS; -- const buckets = []; -- if (range.unit === "hour" || range.unit === "day") { -- const step = range.unit === "hour" ? HOUR_MS : DAY_MS; -- for (let start = first; start < end; start += step) buckets.push({ start, end: start + step, key: range.unit === "hour" ? hourKey(start) : dayKey(start) }); -- } else if (range.unit === "week") { -- for (let monday = mondayOf(first); monday < end; monday += 7 * DAY_MS) { -- buckets.push({ start: Math.max(monday, first), end: Math.min(monday + 7 * DAY_MS, end), key: dayKey(monday) }); -- } -- } else { -- const year = new Date(first).getUTCFullYear(); -- for (let month = new Date(first).getUTCMonth(); Date.UTC(year, month, 1) < end; month += 1) { -- const start = Date.UTC(year, month, 1); -- buckets.push({ start: Math.max(start, first), end: Math.min(Date.UTC(year, month + 1, 1), end), key: dayKey(start).slice(0, 7) }); -+var GROUP_SETTINGS_CSS = ` -+.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -+.bt-ng-step{animation:bt-veil-in .22s ease-out} -+.bt-ng-label{display:inline-flex;align-items:center;gap:6px} -+.bt-ng-picks{display:flex;flex-wrap:wrap;gap:6px} -+.bt-ng-chip{display:inline-flex;align-items:center;gap:6px;height:32px;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:13px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease,scale .3s cubic-bezier(.34,1.56,.64,1)} -+.bt-ng-chip:hover{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-ng-chip:active{scale:.96} -+.bt-ng-chip[aria-checked=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} -+.bt-ng-chip .bt-admin-star{animation:bt-check-pop .4s cubic-bezier(.34,1.56,.64,1)} -+.bt-ng-name{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+@media (prefers-reduced-motion:reduce){.bt-ng,.bt-ng-step,.bt-ng-chip .bt-admin-star{animation:none}} -+.bt-gs-member{gap:8px} -+.bt-gs-who{flex:1;display:flex;align-items:center;gap:8px;min-width:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer} -+.bt-gs-who .bt-row-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-gs-who:hover .bt-row-title{text-decoration:underline;text-underline-offset:2px} -+.bt-gs-add{gap:8px;color:var(--bt-ink-2)} -+.bt-face-marks{display:inline-flex;flex:none;align-items:center;gap:2px} -+.bt-face-marks>.bt-mark{flex:none} -+.bt-face-person{cursor:default} -+.bt-face-person:hover .bt-row-title{text-decoration:none} -+.bt-face-person-text{display:flex;flex-direction:column;min-width:0} -+.bt-face-bots{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:16px;color:var(--bt-ink-3)} -+.bt-face-bot{padding:0;border:0;background:none;color:inherit;font:inherit} -+button.bt-face-bot{cursor:pointer} -+button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-underline-offset:2px} -+.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} -+.bt-bs-stack .bt-seg{align-self:stretch} -+@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} -+`; -+ -+// src/client/palette.js -+var import_react32 = require("react"); -+var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); -+var paletteText = (raw, names) => readableLine(String(raw ?? ""), names); -+var PALETTE_EXIT_MS = 160; -+function CommandPalette({ start, token, roster, actions, useSessions }) { -+ const [query, setQuery] = (0, import_react32.useState)(start ?? ""); -+ const [cursor, setCursor] = (0, import_react32.useState)(0); -+ const [leaving, setLeaving] = (0, import_react32.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 names = botNames(roster); -+ 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 text2 = paletteText(raw, names); -+ const at = text2.toLowerCase().indexOf(wanted); -+ if (at < 0) continue; -+ const from = Math.max(0, at - 24); -+ hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.slice(from, at + wanted.length + 60)]); -+ break; -+ } -+ } - } -- } -- return buckets; -+ return JSON.stringify(hits); -+ }); -+ const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); -+ const sections = []; -+ const people2 = [ -+ ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ -+ key: bot.id, -+ avatar: (0, import_react32.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), -+ title: pickerBotLabel(bot), -+ 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_react32.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -+ title: room2.name, -+ subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), -+ badge: room2.hidden ? t("Hidden") : void 0, -+ run: async () => { -+ if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); -+ actions.openSession(room2.id); -+ } -+ })) -+ ]; -+ if (people2.length) sections.push([t("Bots"), people2]); -+ const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ -+ key: agent.id, -+ avatar: (0, import_react32.createElement)("span", { className: "bt-palette-agent" }, (0, import_react32.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_react32.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react32.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_react32.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -+ { key: "cmd:bot", icon: (0, import_react32.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -+ { key: "cmd:group", icon: (0, import_react32.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -+ { key: "cmd:settings", icon: (0, import_react32.createElement)(GearIcon), title: t("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_react32.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(); -+ }; -+ let index = -1; -+ return (0, import_react32.createElement)( -+ "div", -+ { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react32.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -+ (0, import_react32.createElement)( -+ "div", -+ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -+ (0, import_react32.createElement)( -+ "div", -+ { className: "bt-palette-head" }, -+ (0, import_react32.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react32.createElement)(SearchIcon)), -+ (0, import_react32.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_react32.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { -+ setQuery(""); -+ setCursor(0); -+ } }, (0, import_react32.createElement)(CloseIcon)) : null -+ ), -+ (0, import_react32.createElement)( -+ "div", -+ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, -+ (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ rows.length === 0 ? (0, import_react32.createElement)( -+ "div", -+ { className: "bt-palette-empty" }, -+ (0, import_react32.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react32.createElement)(SearchIcon)), -+ (0, import_react32.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -+ (0, import_react32.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -+ ) : sections.map(([title, items]) => (0, import_react32.createElement)( -+ import_react32.Fragment, -+ { key: title }, -+ (0, import_react32.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), -+ items.map((row) => { -+ index += 1; -+ const at = index; -+ return (0, import_react32.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_react32.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react32.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -+ (0, import_react32.createElement)( -+ "span", -+ { className: "bt-palette-text" }, -+ (0, import_react32.createElement)("span", { className: "bt-palette-title" }, row.title), -+ row.subtitle ? (0, import_react32.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null -+ ), -+ row.badge ? (0, import_react32.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null -+ ); -+ }) -+ )) -+ ) -+ ) -+ ); - } --function spanOf(range, now) { -- if (range.from) { -- return { buckets: spanBuckets(range), from: `${range.from} 00`, to: `${range.to} 23`, before: hourKey(hourMs(range.from) - range.days * DAY_MS) }; -- } -- const buckets = bucketsOf(range, now); -- return { buckets: buckets.slice(range.count), from: hourKey(buckets[range.count].start), to: now, before: hourKey(buckets[0].start) }; -+ -+// src/client/bot-draft.js -+function freshBotName(roster, base = "New Bot") { -+ const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); -+ if (!taken.has(base.toLowerCase())) return base; -+ let index = 2; -+ while (taken.has(`${base.toLowerCase()} ${index}`)) index += 1; -+ return `${base} ${index}`; - } --function bucketsOf(range, now) { -- const end = hourMs(now); -- const today = end - end % DAY_MS; -- const buckets = []; -- for (let back = range.count * 2 - 1; back >= 0; back -= 1) { -- let start; -- if (range.unit === "hour") start = end - back * HOUR_MS; -- else if (range.unit === "day") start = today - back * DAY_MS; -- else if (range.unit === "week") start = mondayOf(today) - back * 7 * DAY_MS; -- else start = Date.UTC(new Date(end).getUTCFullYear(), new Date(end).getUTCMonth() - back, 1); -- buckets.push({ start, key: range.unit === "hour" ? hourKey(start) : range.unit === "month" ? dayKey(start).slice(0, 7) : dayKey(start) }); -- } -- return buckets; --} --var tokensOf = (row) => row[4] + row[5] + row[6] + row[7]; --var blank2 = () => ({ input: 0, cacheRead: 0, output: 0, calls: 0, total: 0 }); --function addRow(sum, row) { -- sum.input += row[4] + row[6]; -- sum.cacheRead += row[5]; -- sum.output += row[7]; -- sum.calls += row[8]; -- sum.total += tokensOf(row); -- return sum; --} --function addTo(map, key, row) { -- map.set(key, addRow(map.get(key) ?? blank2(), row)); --} --function summarize(view, range, keep = () => true, seriesOf = () => "all") { -- const { buckets, from, to, before } = spanOf(range, view.now); -- const index = new Map(buckets.map((bucket, at) => [bucket.key, at])); -- const weeks = /* @__PURE__ */ new Map(); -- const keyOf = (hour) => { -- if (range.unit === "hour") return hour; -- if (range.unit === "day") return hour.slice(0, 10); -- if (range.unit === "month") return hour.slice(0, 7); -- const day = hour.slice(0, 10); -- let week = weeks.get(day); -- if (week === void 0) weeks.set(day, week = dayKey(mondayOf(hourMs(day)))); -- return week; -- }; -- const current = buckets.map((bucket) => ({ ...bucket, sum: blank2(), stacks: /* @__PURE__ */ new Map() })); -- const result = { buckets: current, total: blank2(), previous: 0, owners: /* @__PURE__ */ new Map(), models: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map() }; -- for (const row of view.rows) { -- const hour = row[HOUR]; -- if (hour < before || hour > to || !keep(row)) continue; -- if (hour < from) { -- result.previous += tokensOf(row); -- continue; -- } -- const at = index.get(keyOf(hour)); -- if (at === void 0) continue; -- const bucket = current[at]; -- addRow(bucket.sum, row); -- addRow(result.total, row); -- const series = seriesOf(row); -- bucket.stacks.set(series, (bucket.stacks.get(series) ?? 0) + tokensOf(row)); -- addTo(result.owners, row[OWNER], row); -- addTo(result.models, row[MODEL], row); -- addTo(result.kinds, row[KIND], row); -- } -- return result; -+function startModel(roster) { -+ const models = roster.models ?? []; -+ const served = (ref) => ref ? models.find((model) => model.ref === ref) : void 0; -+ const main = roster.byId?.[roster.mainBotId] ?? (roster.bots ?? []).find((bot) => bot.id === roster.mainBotId); -+ return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? ""; - } --function trimmed2(value) { -- const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); -- return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; -+function createPayload({ name, look, image, model, effort }) { -+ const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } }; -+ if (model) payload.model = model; -+ if (effort) payload.effort = effort; -+ return payload; - } --function compact(count) { -- for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { -- if (count >= size * 0.9995) return `${trimmed2(count / size)}${unit}`; -+async function afterCreate({ id, text: text2, actions }) { -+ actions.openSession(id); -+ actions.showDetails(id, "settings"); -+ const line2 = String(text2 ?? "").trim(); -+ if (line2 === "") return; -+ try { -+ await actions.send(id, line2); -+ } catch (failure) { -+ actions.notify?.(failure?.message ?? String(failure)); - } -- return String(Math.round(count)); --} --function percent(part, whole) { -- if (!(whole > 0) || !(part > 0)) return "0"; -- const value = part / whole * 100; -- return value < 1 ? "<1" : String(Math.round(value)); - } - --// src/client/usage-page.js --var import_react37 = require("react"); --var HEAT_WEEKS = 53; --var TOP_SERIES = 7; --var RING_R = 42; --var RING2 = 2 * Math.PI * RING_R; --var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; --var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; --var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; --var PARTS = [["output", 1], ["input", 0.6], ["cacheRead", 0.32]]; --var partLabel = (part) => ({ output: t("Output"), input: t("Input"), cacheRead: t("Cache hit") })[part]; --var soften = (paint, strength = 1) => { -- const mix = (color) => `color-mix(in srgb, ${color} ${Math.round(88 * strength)}%, var(--bt-card))`; -- return paint.startsWith("linear-gradient(") ? paint.replace(/#[0-9a-f]{6}/gi, mix) : mix(paint); -+// src/client/create-bot.js -+var import_react33 = require("react"); -+var pick = (list) => list[Math.floor(Math.random() * list.length)]; -+var randomLook = (avoid = {}) => { -+ const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); -+ const colors = DEFAULT_COLORS.filter((color) => color !== avoid.color); -+ return { shape: pick(shapes), color: pick(colors) }; - }; --var full = (count) => new Intl.NumberFormat(dateLocale()).format(Math.round(count)); --var hitRate = (sum) => percent(sum.cacheRead, sum.input + sum.cacheRead); --var dates = (options) => new Intl.DateTimeFormat(dateLocale(), { timeZone: "UTC", ...options }); --var hourText = (ms) => `${String(new Date(ms).getUTCHours()).padStart(2, "0")}:00`; --var yearOf = (ms) => new Date(ms).getUTCFullYear(); --function spanText(from, to) { -- const start = hourMs(from); -- const end = hourMs(to); -- const year = (/* @__PURE__ */ new Date()).getFullYear(); -- const format = dates(yearOf(start) !== year || yearOf(end) !== year ? { year: "numeric", month: "short", day: "numeric" } : { month: "short", day: "numeric" }); -- return from === to ? format.format(start) : `${format.format(start)} – ${format.format(end)}`; --} --var daysText = (count) => count === 1 ? t("1 day") : t("{count} days", { count }); --var lastText = (range) => range.from ? spanText(range.from, range.to) : t("Last {range}", { range: rangeLabel(range.id) }); --function compareText(range) { -- if (!range.from) return t("Compared with the {range} before", { range: rangeLabel(range.id) }); -- return range.days === 1 ? t("Compared with the day before") : t("Compared with the {count} days before", { count: range.days }); --} --function tickOf(range, bucket) { -- if (range.unit === "hour") { -- return range.days > 1 && new Date(bucket.start).getUTCHours() === 0 ? dates({ month: "numeric", day: "numeric" }).format(bucket.start) : hourText(bucket.start); -- } -- if (range.unit === "month") return dates({ month: "short" }).format(bucket.start); -- return dates({ month: "numeric", day: "numeric" }).format(bucket.start); --} --function titleOf(range, bucket) { -- if (range.unit === "hour") return `${dates({ month: "short", day: "numeric" }).format(bucket.start)} ${hourText(bucket.start)}–${hourText(bucket.start + HOUR_MS)}`; -- if (range.unit === "day") return dates({ month: "long", day: "numeric", weekday: "short" }).format(bucket.start); -- if (range.unit === "week") return spanText(dayKey(bucket.start), dayKey((bucket.end ?? bucket.start + 7 * DAY_MS) - DAY_MS)); -- if (bucket.end && (new Date(bucket.start).getUTCDate() !== 1 || new Date(bucket.end).getUTCDate() !== 1)) return spanText(dayKey(bucket.start), dayKey(bucket.end - DAY_MS)); -- return dates({ year: "numeric", month: "long" }).format(bucket.start); --} --function useTween(value) { -- const [shown, setShown] = (0, import_react37.useState)(0); -- const from = (0, import_react37.useRef)(0); -- (0, import_react37.useEffect)(() => { -- if (reducedMotion() || typeof requestAnimationFrame !== "function") { -- from.current = value; -- setShown(value); -- return void 0; -+function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { -+ const [look, setLook] = (0, import_react33.useState)(() => randomLook()); -+ const [image, setImage] = (0, import_react33.useState)(null); -+ const [name, setName] = (0, import_react33.useState)(start?.name ?? ""); -+ const [model, setModel] = (0, import_react33.useState)(() => startModel(roster)); -+ const [effort, setEffort] = (0, import_react33.useState)(void 0); -+ const [busy, setBusy] = (0, import_react33.useState)(false); -+ const [error, setError] = (0, import_react33.useState)(""); -+ const fileRef = (0, import_react33.useRef)(null); -+ const frame = (0, import_react33.useRef)(null); -+ useEscape(onBack); -+ (0, import_react33.useEffect)(() => { -+ if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); -+ }, []); -+ const fallbackName = freshBotName(roster, t("New Bot")); -+ const finalName = name.trim() || fallbackName; -+ const clash = roster.bots.some((bot) => bot.name.toLowerCase() === finalName.toLowerCase()); -+ const problem = nameProblem(finalName); -+ const create = async () => { -+ if (busy || clash || problem) return; -+ setError(""); -+ setBusy(true); -+ try { -+ const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort })); -+ await onCreated(bot); -+ } catch (failure) { -+ setError(failure?.message ?? String(failure)); -+ setBusy(false); - } -- const begin = from.current; -- const started = performance.now(); -- let frame; -- const step = (now) => { -- const progress = Math.min(1, (now - started) / 650); -- from.current = begin + (value - begin) * (1 - (1 - progress) ** 3); -- setShown(from.current); -- if (progress < 1) frame = requestAnimationFrame(step); -- }; -- frame = requestAnimationFrame(step); -- return () => cancelAnimationFrame(frame); -- }, [value]); -- return shown; --} --var glyphBox = (size, icon3) => (0, import_react37.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react37.createElement)(icon3)); --function ModelGlyph({ name, paint, size }) { -- return (0, import_react37.createElement)("span", { -- className: "bt-us-model", -- "aria-hidden": true, -- style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -- }, (name.match(/[\p{L}\p{N}]/u)?.[0] ?? "?").toUpperCase()); --} --function ownerFace(owner, roster) { -- if (owner.kind === "bot") { -- const bot = roster.byId[owner.id] ?? { id: owner.id, name: owner.name }; -- const name = bot.name || owner.name || t("Deleted Bot"); -- const role = bot.role?.trim() ?? ""; -- return { -- kind: "bot", -- name, -- sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, -- paint: paintCss(colorOf(bot)), -- stops: GRADIENTS[colorOf(bot)], -- glyph: (size) => (0, import_react37.createElement)(BotAvatar, { bot, size, badge: false, live: false }) -- }; -- } -- if (owner.kind === "room") { -- const room2 = roster.roomsById[owner.id]; -- return { -- kind: "room", -- name: owner.name || t("Deleted group chat"), -- sub: owner.deleted ? t("Deleted") : t("Group chat"), -- paint: "var(--bt-us-rest)", -- glyph: (size) => room2 ? (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) -- }; -- } -- if (owner.kind === "agent") { -- return { -- kind: "agent", -- name: owner.name || "DSH Agent", -- sub: owner.deleted ? t("Deleted") : "", -- paint: "var(--bt-us-rest)", -- glyph: (size) => glyphBox(size, ChatsIcon) -- }; -- } -- return { kind: "other", name: t("Other"), sub: "", paint: "var(--bt-us-rest)", glyph: (size) => glyphBox(size, ChatsIcon) }; --} --function kindLabel(kind, botFocus) { -- const labels = { chat: botFocus ? t("Own chat") : t("Conversations"), group: t("Group chats"), compaction: t("Compaction and handoff"), image: t("Image reading"), memory: t("Memory"), "session-title": t("Titles"), other: t("Other") }; -- return labels[kind] ?? kind; --} --function Tabs({ label, value, options, onChange, boxRef }) { -- const names = options.map(([, text2]) => text2).join("\n"); -- const glide = useGlide(`${value} --${names}`, { box: boxRef }); -- return (0, import_react37.createElement)( -+ }; -+ const upload = (file) => { -+ if (!file) return; -+ setError(""); -+ avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); -+ }; -+ const previewLook = image ? { ...look, image } : look; -+ const card = (0, import_react33.createElement)( - "div", -- { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, -- (0, import_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- options.map(([id, text2, icon3]) => (0, import_react37.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react37.createElement)(icon3) : null, text2)) -- ); --} --function Stats({ range, total, previous }) { -- const shown = useTween(total.total); -- const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; -- const cell = (label, value) => (0, import_react37.createElement)("div", { className: "bt-us-stat" }, (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react37.createElement)("span", { className: "bt-us-stat-value" }, value)); -- return (0, import_react37.createElement)( -- "section", -- { className: "bt-us-stats", style: { "--i": 0 } }, -- (0, import_react37.createElement)( -+ { className: "bt-create-card" }, -+ (0, import_react33.createElement)( - "div", -- { className: "bt-us-stat bt-us-stat-main" }, -- (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), -- (0, import_react37.createElement)( -+ { className: "bt-create-preview" }, -+ // The key remounts the mark, so every new pick plays the pop. -+ (0, import_react33.createElement)( - "span", -- { className: "bt-us-stat-value" }, -- compact(shown), -- (0, import_react37.createElement)("small", null, t("tokens")), -- change === null ? null : (0, import_react37.createElement)( -- "span", -- { className: "bt-us-delta", title: compareText(range) }, -- `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -- ) -- ) -- ), -- cell(t("Cache hit rate"), `${hitRate(total)}%`), -- cell(t("Output"), compact(total.output)), -- cell(t("Calls"), full(total.calls)) -- ); --} --function niceTop(max) { -- if (!(max > 0)) return 1; -- const step = 10 ** Math.floor(Math.log10(max)); -- return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); --} --function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { -- const [hover, setHover] = (0, import_react37.useState)(null); -- const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); -- const count = columns.length; -- const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); -- const tipOf = new Map(tipSeries.map((entry) => [entry.key, entry])); -- const tip = hover === null ? null : columns[hover]; -- const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; -- return (0, import_react37.createElement)( -- "section", -- { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-card-head" }, -- (0, import_react37.createElement)("h3", null, t("Tokens over time"), (0, import_react37.createElement)("span", null, unitLabel(range.unit))), -- onBy ? (0, import_react37.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null -+ { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, -+ (0, import_react33.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) -+ ), -+ (0, import_react33.createElement)("span", { className: "bt-create-name" }, finalName) - ), -- (0, import_react37.createElement)( -+ (0, import_react33.createElement)("div", { className: "bt-create-step" }, t("Look")), -+ (0, import_react33.createElement)( - "div", -- { className: "bt-us-chart-body" }, -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, -- [1, 0.5].map((level) => (0, import_react37.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react37.createElement)("span", null, compact(top * level)))), -- (0, import_react37.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react37.createElement)("span", null, "0")), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-cols" }, -- columns.map((column, at) => (0, import_react37.createElement)("div", { -- key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, -- className: "bt-us-col", -- "data-on": hover === at ? "" : void 0, -- onMouseEnter: () => setHover(at) -- }, column.total > 0 ? (0, import_react37.createElement)( -- "div", -- { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, -- column.parts.map(([key, value]) => value > 0 ? (0, import_react37.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) -- ) : (0, import_react37.createElement)("div", { className: "bt-us-stub" }))) -- ), -- tip ? (0, import_react37.createElement)( -- "div", -- { className: "bt-us-tip", style: tipSide }, -- (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, tip.title), -- (0, import_react37.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), -- tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react37.createElement)( -- "div", -- { key, className: "bt-us-tip-row" }, -- (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), -- (0, import_react37.createElement)("span", null, tipOf.get(key)?.name), -- (0, import_react37.createElement)("b", null, compact(value)) -- )) -- ) : null -- ), -- (0, import_react37.createElement)( -+ { className: "bt-create-looks" }, -+ (0, import_react33.createElement)(CharacterGrid, { -+ look, -+ hasPhoto: Boolean(image), -+ onShape: (shape) => { -+ setImage(null); -+ setLook((current) => ({ ...current, shape })); -+ }, -+ onColor: (color) => { -+ setImage(null); -+ setLook((current) => ({ ...current, color })); -+ } -+ }), -+ (0, import_react33.createElement)( - "div", -- { className: "bt-us-ticks", "aria-hidden": true }, -- // A preset counts its labels back from now; a picked span forward from its first day. -- columns.map((column, at) => (0, import_react37.createElement)( -- "span", -- { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, -- (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" -- )) -+ { className: "bt-actions bt-create-lookbar" }, -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ setImage(null); -+ setLook((current) => randomLook(current)); -+ } }, t("Shuffle")), -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), -+ image ? (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, -+ (0, import_react33.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ const file = event.target.files?.[0]; -+ event.target.value = ""; -+ upload(file); -+ } }) - ) - ), -- series.length > 1 || series[0]?.pick ? (0, import_react37.createElement)( -- "div", -- { className: "bt-us-legend" }, -- series.map((entry) => (0, import_react37.createElement)(entry.pick ? "button" : "span", { -- key: entry.key, -- className: "bt-us-key", -- ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} -- }, (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) -- ) : null -- ); --} --function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick }) { -- const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; -- return (0, import_react37.createElement)( -- onClick ? "button" : "div", -- { -- className: "bt-us-row", -- "data-kind": kind, -- style: { "--i": index }, -- title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), -- ...onClick ? { type: "button", onClick } : {} -- }, -- glyph ? (0, import_react37.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, -- (0, import_react37.createElement)( -- "span", -- { className: "bt-us-row-body" }, -- (0, import_react37.createElement)( -- "span", -- { className: "bt-us-row-line" }, -- (0, import_react37.createElement)("span", { className: "bt-us-row-name" }, name), -- sub ? (0, import_react37.createElement)("span", { className: "bt-us-row-sub" }, sub) : null -- ), -- (0, import_react37.createElement)("span", { className: "bt-us-track" }, (0, import_react37.createElement)("span", { style: { width: `${width}%`, background: fill3 } })) -+ (0, import_react33.createElement)( -+ "label", -+ { className: "bt-field" }, -+ (0, import_react33.createElement)("span", { className: "bt-field-label" }, t("Name")), -+ (0, import_react33.createElement)("input", { -+ className: "bt-input", -+ autoFocus: !inline, -+ value: name, -+ placeholder: fallbackName, -+ maxLength: 40, -+ "aria-label": t("Name"), -+ onChange: (event) => setName(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" && !event.nativeEvent.isComposing) { -+ event.preventDefault(); -+ void create(); -+ } -+ } -+ }), -+ problem ? (0, import_react33.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react33.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null - ), -- (0, import_react37.createElement)("span", { className: "bt-us-row-num" }, (0, import_react37.createElement)("b", null, compact(sum.total)), (0, import_react37.createElement)("small", null, `${percent(sum.total, whole)}%`)) -- ); --} --function ListCard({ title, count, index, children }) { -- return (0, import_react37.createElement)( -- "section", -- { className: "bt-us-card", style: { "--i": index } }, -- (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, title, count === void 0 ? null : (0, import_react37.createElement)("span", null, count))), -- (0, import_react37.createElement)("div", { className: "bt-us-rows" }, children) -- ); --} --function Profile({ face, share, range }) { -- const arc = Math.max(0, Math.min(1, share)) * RING2; -- return (0, import_react37.createElement)( -- "section", -- { className: "bt-us-profile" }, -- (0, import_react37.createElement)( -- "span", -- { className: "bt-us-ring" }, -- (0, import_react37.createElement)( -- "svg", -- { viewBox: "0 0 96 96", "aria-hidden": true }, -- face.stops ? (0, import_react37.createElement)("defs", null, (0, import_react37.createElement)( -- "linearGradient", -- { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, -- face.stops.map(([offset, color]) => (0, import_react37.createElement)("stop", { key: offset, offset, stopColor: color })) -- )) : null, -- (0, import_react37.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), -- arc > 0 ? (0, import_react37.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null -- ), -- (0, import_react37.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) -+ (0, import_react33.createElement)( -+ "div", -+ { className: "bt-field" }, -+ (0, import_react33.createElement)("span", { className: "bt-field-label" }, t("Model")), -+ (0, import_react33.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { -+ setModel(ref); -+ setEffort(void 0); -+ }, onPickEffort: setEffort }), -+ (0, import_react33.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) - ), -- (0, import_react37.createElement)( -+ error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ (0, import_react33.createElement)( - "div", -- { className: "bt-us-profile-copy" }, -- (0, import_react37.createElement)("h3", null, face.name), -- face.sub ? (0, import_react37.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, -- (0, import_react37.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) -+ { className: "bt-create-foot" }, -+ // The dialog's header already has Back and Close (#230); only the inline form needs its own. -+ inline ? (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) - ) - ); --} --var monthOf = (ms) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth(), 1); --var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); --function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { -- const box = (0, import_react37.useRef)(null); -- const todayMs = hourMs(today); -- const thisMonth = monthOf(todayMs); -- const [right, setRight] = (0, import_react37.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); -- const [anchor, setAnchor] = (0, import_react37.useState)(null); -- const [hover, setHover] = (0, import_react37.useState)(null); -- (0, import_react37.useEffect)(() => { -- const onDown = (event) => { -- if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- onClose(); -- } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, []); -- const [low, high] = anchor ? [anchor, hover ?? anchor].sort() : span ? [span.from, span.to] : [null, null]; -- const press = (key) => { -- if (!anchor) { -- setAnchor(key); -- return; -- } -- const [from, to] = [anchor, key].sort(); -- onPick(from, to); -- }; -- const weekdays = (0, import_react37.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); -- const month = (start, side) => { -- const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); -- const lead = (new Date(start).getUTCDay() + 6) % 7; -- const cells = Array.from({ length: lead }, (_, at) => (0, import_react37.createElement)("span", { key: `b${at}` })); -- for (let day = 1; day <= length; day += 1) { -- const ms = start + (day - 1) * DAY_MS; -- const key = dayKey(ms); -- const weekday = (lead + day - 1) % 7; -- const flag = (on) => on ? "" : void 0; -- cells.push((0, import_react37.createElement)("button", { -- key, -- type: "button", -- className: "bt-us-day", -- disabled: ms > todayMs, -- "aria-label": dates({ year: "numeric", month: "long", day: "numeric", weekday: "short" }).format(ms), -- "aria-pressed": key === low || key === high, -- "data-in": flag(low !== null && key >= low && key <= high), -- "data-start": flag(key === low), -- "data-end": flag(key === high), -- "data-l": flag(weekday === 0 || day === 1), -- "data-r": flag(weekday === 6 || day === length), -- "data-today": flag(key === today), -- "data-has": flag(active.has(key)), -- onMouseEnter: () => setHover(key), -- onClick: () => press(key) -- }, (0, import_react37.createElement)("b", null, day))); -- } -- const nav = side === "left" ? (0, import_react37.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react37.createElement)(ChevronLeftIcon)) : (0, import_react37.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react37.createElement)(ChevronRightIcon)); -- return (0, import_react37.createElement)( -- "div", -- { key: start, className: "bt-us-month" }, -- (0, import_react37.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react37.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), -- (0, import_react37.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react37.createElement)("span", { key: at }, name))), -- (0, import_react37.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) -- ); -- }; -- const shortcuts = [ -- ["month", t("This month"), dayKey(thisMonth), today], -- ["last", t("Last month"), dayKey(addMonths(thisMonth, -1)), dayKey(thisMonth - DAY_MS)], -- ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] -- ]; -- const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; -- return (0, import_react37.createElement)( -+ if (inline) return (0, import_react33.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); -+ return (0, import_react33.createElement)( - "div", -- { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, -- (0, import_react37.createElement)( -+ { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, -+ (0, import_react33.createElement)( - "div", -- { className: "bt-us-pick-quick" }, -- shortcuts.map(([id, label, from, to]) => (0, import_react37.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": span?.from === from && span?.to === to, -- onClick: () => onPick(from, to) -- }, label)) -+ { className: "bt-create-head" }, -+ canGoBack ? (0, import_react33.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react33.createElement)(ChevronLeftIcon)) : (0, import_react33.createElement)("span", { className: "bt-create-gap" }), -+ (0, import_react33.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react33.createElement)(CloseIcon)) - ), -- (0, import_react37.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-pick-foot", role: "status" }, -- (0, import_react37.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), -- low === null ? null : (0, import_react37.createElement)("span", null, (0, import_react37.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) -- ) -+ (0, import_react33.createElement)("div", { className: "bt-create-body" }, card) - ); - } --function Heat({ view, keep, paint, index }) { -- const [hover, setHover] = (0, import_react37.useState)(null); -- const today = hourMs(`${view.now.slice(0, 10)} 00`); -- const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; -- const from = dayKey(first); -- const daily = /* @__PURE__ */ new Map(); -- for (const row of view.rows) { -- const day = row[HOUR].slice(0, 10); -- if (day >= from && keep(row)) daily.set(day, (daily.get(day) ?? 0) + tokensOf(row)); -- } -- const most = Math.max(0, ...daily.values()); -- const levels = [0.3, 0.52, 0.76, 1]; -- const months = []; -- const cells = []; -- for (let week = 0; week < HEAT_WEEKS; week += 1) { -- const monday = first + week * 7 * DAY_MS; -- if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { -- months.push((0, import_react37.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); -- } -- for (let day = 0; day < 7; day += 1) { -- const ms = monday + day * DAY_MS; -- const value = daily.get(dayKey(ms)) ?? 0; -- const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; -- cells.push((0, import_react37.createElement)("span", { -- key: ms, -- className: "bt-us-cell", -- style: { "--w": week }, -- "data-on": hover?.ms === ms ? "" : void 0, -- "data-future": ms > today ? "" : void 0, -- "data-today": ms === today ? "" : void 0, -- onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) -- }, level > 0 ? (0, import_react37.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); -+ -+// src/client/new-chat.js -+var import_react34 = require("react"); -+function NewChat({ mode, roster, actions }) { -+ const [query, setQuery] = (0, import_react34.useState)(""); -+ const [picked, setPicked] = (0, import_react34.useState)([]); -+ const [group, setGroup] = (0, import_react34.useState)(mode === "group"); -+ const [compose, setCompose] = (0, import_react34.useState)(mode !== "new"); -+ const [admin, setAdmin] = (0, import_react34.useState)(null); -+ const [message, setMessage] = (0, import_react34.useState)(""); -+ const [busy, setBusy] = (0, import_react34.useState)(false); -+ const [error, setError] = (0, import_react34.useState)(""); -+ const [fix, setFix] = (0, import_react34.useState)(null); -+ const [fixNote, setFixNote] = (0, import_react34.useState)(""); -+ const [inviteLink, setInviteLink] = (0, import_react34.useState)(""); -+ const [cursor, setCursor] = (0, import_react34.useState)(0); -+ const [draft, setDraft] = (0, import_react34.useState)(mode === "create" ? { name: "", fromList: false } : null); -+ const inputRef = (0, import_react34.useRef)(null); -+ const close = () => actions.closeOverlay(); -+ useEscape(close); -+ const paneLeft = usePaneLeft(); -+ const wanted = query.trim().toLowerCase(); -+ const bots = listedChatBots(roster); -+ const choosingAdmin = group && admin === null; -+ const pool = choosingAdmin ? bots.filter((bot) => !bot.remote) : bots; -+ const matches = pool.filter((bot) => !picked.includes(bot.id) && bot.id !== admin && (wanted === "" || bot.name.toLowerCase().includes(wanted) || pickerBotLabel(bot).toLowerCase().includes(wanted) || (bot.role ?? "").toLowerCase().includes(wanted))); -+ const exact = bots.some((bot) => bot.name.toLowerCase() === wanted); -+ const pickedBots = picked.map((id) => roster.byId[id]).filter(Boolean); -+ const run = async (task) => { -+ if (busy) return; -+ setError(""); -+ setFix(null); -+ setFixNote(""); -+ setInviteLink(""); -+ setBusy(true); -+ try { -+ await task(); -+ } catch (failure) { -+ setError(failure?.message ?? String(failure)); -+ setFix(ungrantedLocalFix(failure?.details) ?? null); -+ } finally { -+ setBusy(false); - } -- } -- const tipSide = hover ? { -- ...hover.week >= HEAT_WEEKS / 2 ? { right: `calc(${(HEAT_WEEKS - hover.week) / HEAT_WEEKS * 100}% + 8px)` } : { left: `calc(${(hover.week + 1) / HEAT_WEEKS * 100}% + 8px)` }, -- ...hover.day < 4 ? { top: `calc(${hover.day / 7 * 100}% - 2px)` } : { top: "auto", bottom: `calc(${(6 - hover.day) / 7 * 100}% - 2px)` } -- } : null; -- const active = [...daily.values()].filter((value) => value > 0).length; -- const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); -- return (0, import_react37.createElement)( -- "section", -- { className: "bt-us-card bt-us-heat", style: { "--i": index } }, -- (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, t("Activity")), (0, import_react37.createElement)("span", { className: "bt-us-head-note" }, summary)), -- (0, import_react37.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, -- (0, import_react37.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), -- hover ? (0, import_react37.createElement)( -- "div", -- { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, -- (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), -- (0, import_react37.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) -- ) : null -- ), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-heat-scale", "aria-hidden": true }, -- t("Light"), -- levels.map((level) => (0, import_react37.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react37.createElement)("i", { style: { background: soften(paint, level) } }))), -- t("Heavy") -- ) -- ); --} --function Skeleton2() { -- return (0, import_react37.createElement)( -- "div", -- { className: "bt-us", "aria-busy": true }, -- (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-sheet bt-us-skel-sheet" }, -- (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), -- (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-pair" }, -- (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), -- (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) -- ) -- ) -- ); --} --var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null; --function UsagePage({ roster, actions, focusId }) { -- const [view, setView] = (0, import_react37.useState)(null); -- const [error, setError] = (0, import_react37.useState)(""); -- const [rangeId, setRangeId] = (0, import_react37.useState)("7d"); -- const [span, setSpan] = (0, import_react37.useState)(null); -- const [picking, setPicking] = (0, import_react37.useState)(false); -- const [by, setBy] = (0, import_react37.useState)("bot"); -- const tabs = (0, import_react37.useRef)(null); -- const focus = focusOf(focusId); -- (0, import_react37.useEffect)(() => { -- let stopped = false; -- let timer; -- const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -- }))); -- const load = async () => { -- try { -- const value = await actions.usage(); -- await resized; -- if (stopped) return; -- (0, import_react37.startTransition)(() => setView(value)); -- setError(""); -- timer = setTimeout(load, value.scanning ? 3e3 : 2e4); -- } catch (failure) { -- if (stopped) return; -- setError(failure?.message ?? String(failure)); -- timer = setTimeout(load, 1e4); -- } -- }; -- void load(); -- return () => { -- stopped = true; -- clearTimeout(timer); -- }; -- }, []); -- const today = view?.now.slice(0, 10); -- const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); -- const shaped = (0, import_react37.useMemo)(() => { -- if (!view) return null; -- const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); -- const lifetime = /* @__PURE__ */ new Map(); -- for (const row of view.rows) lifetime.set(row[MODEL], (lifetime.get(row[MODEL]) ?? 0) + tokensOf(row)); -- const modelPaint = new Map([...lifetime].sort((a, b) => b[1] - a[1]).map(([model], rank) => [model, `var(--bt-ink-${MODEL_INKS[rank % MODEL_INKS.length]})`])); -- const models2 = view.models.map((model, at) => { -- const name = model.name || model.model || t("Unknown model"); -- const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; -- return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react37.createElement)(ModelGlyph, { name, paint, size }) }; -- }); -- const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; -- const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -- const isBot2 = (at) => ["bot", "agent"].includes(view.owners[at]?.kind); -- const all2 = summarize(view, range, void 0, (row) => by === "bot" ? isBot2(row[OWNER]) ? `o${row[OWNER]}` : "rest" : `m${row[MODEL]}`); -- const keep2 = ownerAt2 >= 0 ? (row) => row[OWNER] === ownerAt2 : modelAt >= 0 ? (row) => row[MODEL] === modelAt : () => true; -- const face2 = ownerAt2 >= 0 ? faces2[ownerAt2] : modelAt >= 0 ? models2[modelAt] : null; -- const part2 = face2 ? summarize(view, range, keep2) : all2; -- const activeDays2 = /* @__PURE__ */ new Set(); -- let first = view.now; -- for (const row of view.rows) { -- if (row[HOUR] < first) first = row[HOUR]; -- if (keep2(row) && tokensOf(row) > 0) activeDays2.add(row[HOUR].slice(0, 10)); -+ }; -+ const leaveOut = (id) => { -+ if (id === admin) setAdmin(null); -+ setPicked((current) => current.filter((other) => other !== id)); -+ setError(""); -+ setFix(null); -+ setFixNote(""); -+ setInviteLink(""); -+ }; -+ const copyInvite = async (bot) => { -+ const social = globalThis.__flybot__?.social; -+ let link; -+ try { -+ const invite = await social.createInvite(bot.id); -+ link = invite?.link || invite?.url; -+ if (!link) throw new Error("no link"); -+ } catch { -+ setInviteLink(""); -+ setFixNote(t("Could not create the invite. Use Bot settings → Sharing → Copy invite link for this Bot.")); -+ return; - } -- const oldest2 = Math.min(monthOf(hourMs(first)), addMonths(monthOf(hourMs(view.now)), -12)); -- const ranked2 = (map) => [...map].filter(([, sum]) => sum.total > 0).sort((a, b) => b[1].total - a[1].total); -- let series2; -- if (face2) { -- series2 = [{ key: "all", name: face2.name, fill: soften(face2.paint) }]; -+ if (await copiedToClipboard(link)) { -+ setInviteLink(""); -+ setFixNote(t("Invite link for {name} copied. Send it to your friend; once they accept, create the group again.", { name: bot.name })); - } else { -- if (by === "bot") { -- const bots = ranked2(all2.owners).filter(([at]) => isBot2(at)); -- series2 = bots.slice(0, TOP_SERIES).map(([at]) => ({ key: `o${at}`, name: faces2[at].name, paint: faces2[at].paint, pick: { kind: "owner", key: view.owners[at].key } })); -- if (bots.length > TOP_SERIES) series2.push({ key: "more", name: t("More Bots"), paint: "var(--bt-us-more)" }); -- if (ranked2(all2.owners).some(([at]) => !isBot2(at))) series2.push({ key: "rest", name: t("Other"), paint: "var(--bt-us-rest)" }); -- } else { -- const used = ranked2(all2.models); -- series2 = used.slice(0, TOP_SERIES).map(([at]) => ({ key: `m${at}`, name: models2[at].name, paint: models2[at].paint, pick: { kind: "model", key: view.models[at].key } })); -- if (used.length > TOP_SERIES) series2.push({ key: "more", name: t("More models"), paint: "var(--bt-us-more)" }); -- } -- const repeats = /* @__PURE__ */ new Map(); -- for (const entry of series2) { -- const seen = repeats.get(entry.paint) ?? 0; -- repeats.set(entry.paint, seen + 1); -- entry.fill = soften(entry.paint, [1, 0.6, 0.36][seen % 3]); -- } -+ setInviteLink(link); -+ setFixNote(t("Could not copy the invite link for {name}. Copy the link below and send it to your friend; once they accept, create the group again.", { name: bot.name })); - } -- const tipSeries2 = face2 ? PARTS.map(([key, strength]) => ({ key, name: partLabel(key), fill: soften(face2.paint, strength) })) : series2; -- const fillOf2 = new Map(series2.map((entry) => [entry.key, entry.fill])); -- const shownKeys = new Set(series2.map((entry) => entry.key)); -- const columns2 = part2.buckets.map((bucket) => { -- const parts = face2 ? [["all", bucket.sum.total]] : series2.map((entry) => [entry.key, entry.key === "more" ? [...bucket.stacks].filter(([key]) => key !== "rest" && !shownKeys.has(key)).reduce((sum, [, value]) => sum + value, 0) : bucket.stacks.get(entry.key) ?? 0]); -- const tip = face2 ? PARTS.map(([key]) => [key, bucket.sum[key]]) : parts; -- return { key: bucket.key, tick: tickOf(range, bucket), title: titleOf(range, bucket), total: bucket.sum.total, parts, tip }; -- }); -- return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; -- }, [view, rangeId, span, by, focusId, roster]); -- const root = (0, import_react37.useRef)(null); -- (0, import_react37.useEffect)(() => { -- root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); -- }, [focusId]); -- if (!view) { -- return error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react37.createElement)(Skeleton2); -- } -- if (view.rows.length === 0) { -- return (0, import_react37.createElement)( -- "div", -- { className: "bt-us" }, -- (0, import_react37.createElement)( -- "div", -- { className: "bt-us-empty" }, -- (0, import_react37.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), -- (0, import_react37.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), -- (0, import_react37.createElement)("span", null, t("Token usage shows up here after a model is called.")) -- ) -- ); -- } -- const { faces, models, ownerAt, face, keep, all, part, series, tipSeries, fillOf, columns, ranked, isBot, activeDays, oldest } = shaped; -- const pick2 = (next) => actions.openSettings("usage", `${next.kind}:${next.key}`); -- const chooseRange = (id) => { -- if (id !== "custom") { -- setRangeId(id); -- setPicking(false); -+ }; -+ const pick2 = (bot) => { -+ setPicked((current) => [...current, bot.id]); -+ setQuery(""); -+ setCursor(0); -+ inputRef.current?.focus(); -+ }; -+ const chooseAdmin = (bot) => { -+ setAdmin(bot.id); -+ setQuery(""); -+ setCursor(0); -+ inputRef.current?.focus(); -+ }; -+ const openBot = (bot) => run(async () => { -+ if (choosingAdmin) { -+ chooseAdmin(bot); - return; - } -- if (span) setRangeId("custom"); -- setPicking((open) => !open); -+ if (group || picked.length > 0) { -+ pick2(bot); -+ return; -+ } -+ if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); -+ if (message.trim()) await actions.send(bot.id, message.trim()); -+ actions.openSession(bot.id); -+ close(); -+ }); -+ const createBot = () => { -+ if (busy) return; -+ setError(""); -+ setDraft({ name: query.trim() !== "" && !exact ? query.trim() : "", fromList: true }); - }; -- const pickSpan = (from, to) => { -- setSpan({ from, to }); -- setRangeId("custom"); -- setPicking(false); -+ const created = (bot) => { -+ close(); -+ return afterCreate({ id: bot.id, text: message, actions }); - }; -- const rangeOptions = [ -- ...RANGES.map((item) => [item.id, rangeLabel(item.id)]), -- ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] -- ]; -- const whole = part.total.total; -- const ownerRow = ([at, sum], index, total = whole) => (0, import_react37.createElement)(Row3, { -- key: `o${at}`, -- kind: faces[at].kind, -- glyph: faces[at].glyph(32), -- name: faces[at].name, -- sub: faces[at].sub, -- sum, -- whole: total, -- index, -- fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), -- onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) -- }); -- const modelRow = ([at, sum], index) => (0, import_react37.createElement)(Row3, { -- key: `m${at}`, -- kind: "model", -- glyph: models[at].glyph(32), -- name: models[at].name, -- sub: models[at].sub, -- sum, -- whole, -- index, -- fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), -- onClick: () => pick2({ kind: "model", key: view.models[at].key }) -- }); -- const nothing = (0, import_react37.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); -- let body; -- if (face) { -- const kinds = ranked(part.kinds); -- const usedModels = ranked(part.models); -- const owners = ranked(part.owners); -- body = [ -- (0, import_react37.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), -- (0, import_react37.createElement)( -- "div", -- { key: "sheet", className: "bt-us-sheet" }, -- (0, import_react37.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), -- (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), -- (0, import_react37.createElement)( -- "div", -- { key: "pair", className: "bt-us-pair" }, -- ownerAt >= 0 ? (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react37.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), -- (0, import_react37.createElement)( -- ListCard, -- { title: t("What for"), index: 3 }, -- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react37.createElement)(Row3, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing -- ) -- ), -- (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) -- ) -+ const makeGroup = async () => { -+ if (admin === null || picked.length === 0) return; -+ const text2 = message.trim(); -+ const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); -+ const participants = remotes.length === 0 ? void 0 : [ -+ ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), -+ ...remotes - ]; -- } else { -- const owners = ranked(all.owners); -- const bots = owners.filter(([at]) => isBot(at)); -- const rest = owners.filter(([at]) => !isBot(at)); -- const usedModels = ranked(all.models); -- body = [ -- (0, import_react37.createElement)( -- "div", -- { key: "sheet", className: "bt-us-sheet" }, -- (0, import_react37.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), -- (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), -- (0, import_react37.createElement)( -- "div", -- { key: "pair", className: "bt-us-pair" }, -- (0, import_react37.createElement)( -- ListCard, -- { title: t("Bots"), count: bots.length, index: 2 }, -- owners.length > 0 ? null : nothing, -- bots.map((entry, index) => ownerRow(entry, index)), -- bots.length > 0 && rest.length > 0 ? (0, import_react37.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, -- rest.map((entry, index) => ownerRow(entry, bots.length + index)) -- ), -- (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) -- ), -- (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) -- ) -+ const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); -+ if (text2) await actions.send(room2.id, text2); -+ actions.openSession(room2.id); -+ close(); -+ }; -+ const createGroup = () => run(makeGroup); -+ const sendMessage = () => run(async () => { -+ const text2 = message.trim(); -+ if (group && admin !== null) { -+ await makeGroup(); -+ return; -+ } -+ if (text2 === "" || picked.length === 0) return; -+ if (picked.length === 1) { -+ await actions.send(picked[0], text2); -+ actions.openSession(picked[0]); -+ close(); -+ return; -+ } -+ const { members, remotes } = splitRoomPicks(roster, picked); -+ const participants = remotes.length === 0 ? void 0 : [ -+ ...members.map((id) => ({ kind: "bot", id })), -+ ...remotes - ]; -+ const room2 = await actions.createRoom(members, participants ? { participants } : {}); -+ await actions.send(room2.id, text2); -+ actions.openSession(room2.id); -+ close(); -+ }); -+ const menuOnly = mode === "new" && !compose && !group && picked.length === 0 && wanted === ""; -+ const options = []; -+ if (!group && picked.length === 0 && mode !== "search" && wanted === "") { -+ if (menuOnly) { -+ options.push({ key: "chat", icon: (0, import_react34.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { -+ setCompose(true); -+ setCursor(0); -+ setTimeout(() => inputRef.current?.focus()); -+ } }); -+ } -+ options.push({ key: "new", icon: (0, import_react34.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); -+ options.push({ key: "group", icon: (0, import_react34.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { -+ setGroup(true); -+ setCompose(true); -+ setCursor(0); -+ inputRef.current?.focus(); -+ } }); -+ for (const action of newChatActionRows({ mode, group, picked, query, roster })) { -+ options.push({ -+ key: action.key, -+ icon: action.icon ? (0, import_react34.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react34.createElement)(PeopleIcon) }) : (0, import_react34.createElement)(PeopleIcon), -+ label: t(action.label), -+ hint: action.hint ? t(action.hint) : void 0, -+ run: () => { -+ const done = Promise.resolve().then(() => action.run({ close, actions, roster })); -+ done.then((note) => { -+ if (typeof note === "string" && note) actions.notify?.(note); -+ }).catch((failure) => setError(failure?.message ?? String(failure))); -+ } -+ }); -+ } -+ if ((roster.agents ?? []).length === 0) { -+ options.push({ key: "agent", icon: (0, import_react34.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { -+ await actions.addAgent(); -+ close(); -+ }) }); -+ } - } -- return (0, import_react37.createElement)( -+ if (!menuOnly) { -+ for (const bot of matches) { -+ options.push({ key: bot.id, bot, label: pickerBotLabel(bot), hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); -+ } -+ } -+ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react34.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); -+ const active = Math.min(cursor, Math.max(options.length - 1, 0)); -+ const listed = options.map((option) => option.key).join("\n"); -+ const glide = useGlide(`${active} -+${listed}`, { axis: "y", ...QUICK_EDGES }); -+ useFlip(glide.box, listed, "[data-flip]"); -+ const drop = (0, import_react34.useRef)(null); -+ useMorph(drop, listed); -+ if (draft) { -+ return (0, import_react34.createElement)( -+ "div", -+ { className: "bt-pane", style: { left: paneLeft } }, -+ (0, import_react34.createElement)(CreateBot, { -+ roster, -+ actions, -+ start: draft, -+ canGoBack: draft.fromList, -+ onBack: draft.fromList ? () => { -+ setDraft(null); -+ setTimeout(() => inputRef.current?.focus()); -+ } : close, -+ onCreated: created -+ }) -+ ); -+ } -+ const adminBot = admin === null ? void 0 : roster.byId[admin]; -+ const recipients = adminBot ? [adminBot, ...pickedBots] : pickedBots; -+ const placeholder = recipients.length > 0 ? t("Message {name}", { name: recipients.map((bot) => bot.name).join(t(" and ")) }) : t("Message Bot"); -+ const unpick = () => { -+ if (picked.length > 0) setPicked((current) => current.slice(0, -1)); -+ else if (admin !== null) setAdmin(null); -+ }; -+ return (0, import_react34.createElement)( - "div", -- { ref: root, className: "bt-us", key: focusId ?? "all" }, -- (0, import_react37.createElement)( -+ { -+ className: "bt-pane", -+ style: { left: paneLeft }, -+ "data-menu": menuOnly || void 0, -+ onMouseDown: menuOnly ? (event) => { -+ if (event.target === event.currentTarget) close(); -+ } : void 0 -+ }, -+ (0, import_react34.createElement)( - "div", -- { className: "bt-us-top" }, -- (0, import_react37.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), -- view.scanning ? (0, import_react37.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, -- picking ? (0, import_react37.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null -- ), -- error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -- body -+ { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, -+ // The popover needs no title row: Escape or a press outside closes it. -+ menuOnly ? null : (0, import_react34.createElement)( -+ "div", -+ { className: "bt-to" }, -+ (0, import_react34.createElement)("span", { className: "bt-to-label" }, t("To:")), -+ adminBot ? (0, import_react34.createElement)( -+ "span", -+ { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, -+ (0, import_react34.createElement)(BotMark, { bot: adminBot, size: 16 }), -+ adminBot.name, -+ (0, import_react34.createElement)(AdminStar), -+ (0, import_react34.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") -+ ) : null, -+ choosingAdmin ? (0, import_react34.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react34.createElement)(AdminStar)) : null, -+ pickedBots.map((bot) => (0, import_react34.createElement)( -+ "span", -+ { key: bot.id, className: "bt-token" }, -+ (0, import_react34.createElement)(BotMark, { bot, size: 16 }), -+ bot.name, -+ (0, import_react34.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") -+ )), -+ (0, import_react34.createElement)("input", { -+ ref: inputRef, -+ autoFocus: true, -+ value: query, -+ "aria-label": t("Recipient"), -+ placeholder: mode === "search" ? t("Search Bots…") : choosingAdmin ? t("Choose the group admin") : group ? t("Who to chat with…") : t("Start a chat with…"), -+ 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, options.length - 1)); -+ } else if (event.key === "ArrowUp") { -+ event.preventDefault(); -+ setCursor(Math.max(active - 1, 0)); -+ } else if (event.key === "Enter" && options[active]) { -+ event.preventDefault(); -+ options[active].run(); -+ } else if (event.key === "Backspace" && query === "") unpick(); -+ } -+ }), -+ group && admin !== null ? (0, import_react34.createElement)("button", { -+ type: "button", -+ className: "bt-send bt-to-create", -+ disabled: busy || picked.length === 0, -+ title: picked.length === 0 ? t("Add at least one more Bot") : void 0, -+ onClick: createGroup -+ }, t("Create group chat")) : null, -+ (0, import_react34.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") -+ ), -+ (0, import_react34.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react34.createElement)( -+ "div", -+ { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, -+ (0, import_react34.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react34.createElement)( -+ "button", -+ { -+ key: option.key, -+ type: "button", -+ role: "option", -+ className: "bt-option", -+ "aria-selected": index === active, -+ "data-flip": option.key, -+ onMouseEnter: () => setCursor(index), -+ onClick: option.run -+ }, -+ option.bot ? (0, import_react34.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react34.createElement)("span", { className: "bt-option-icon" }, option.icon), -+ // An action row's description sits under its title and is always there (#283): a long line -+ // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. -+ // A Bot row's short hint stays beside the name and shows on the active row only. -+ option.bot || !option.hint ? (0, import_react34.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react34.createElement)("span", { className: "bt-option-copy" }, (0, import_react34.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react34.createElement)("span", { className: "bt-option-sub" }, option.hint)), -+ option.bot && option.hint && index === active ? (0, import_react34.createElement)("span", { className: "bt-option-hint" }, option.hint) : null -+ )) -+ )), -+ menuOnly ? null : (0, import_react34.createElement)("div", { className: "bt-nc-spacer" }), -+ error && fix ? (0, import_react34.createElement)( -+ "div", -+ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, -+ (0, import_react34.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), -+ (0, import_react34.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ -+ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react34.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { -+ void copyInvite(bot); -+ } }, t("Copy invite link for {name}", { name: bot.name })) : null, -+ (0, import_react34.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) -+ ])), -+ fixNote ? (0, import_react34.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, -+ inviteLink ? (0, import_react34.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null -+ ) : error ? (0, import_react34.createElement)("div", { className: "bt-error" }, error) : null, -+ menuOnly ? null : (0, import_react34.createElement)( -+ "div", -+ { className: "bt-nc-compose" }, -+ (0, import_react34.createElement)( -+ "div", -+ { className: "bt-nc-card" }, -+ (0, import_react34.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react34.createElement)(PlusIcon)), -+ (0, import_react34.createElement)("textarea", { -+ rows: 1, -+ value: message, -+ placeholder, -+ "aria-label": placeholder, -+ onChange: (event) => setMessage(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { -+ event.preventDefault(); -+ sendMessage(); -+ } -+ } -+ }), -+ (0, import_react34.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react34.createElement)(ArrowUpIcon)) -+ ) -+ ) -+ ) - ); - } --var USAGE_CSS = ` --.bt-us{--bt-us-ease:cubic-bezier(.22,1,.36,1);--bt-us-rest:color-mix(in srgb,var(--bt-ink) 30%,var(--bt-card));--bt-us-more:color-mix(in srgb,var(--bt-ink) 16%,var(--bt-card));display:flex;flex-direction:column;gap:14px;padding-bottom:4px} --.bt-us-top{position:relative;z-index:4;display:flex;align-items:center;gap:14px;flex-wrap:wrap} --.bt-us-tabs{position:relative;display:inline-flex;flex:none;padding:3px;border-radius:999px;background:var(--bt-hover)} --.bt-us-tabs button{position:relative;z-index:1;display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 12px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;line-height:26px;white-space:nowrap;cursor:pointer;transition:color .15s ease} --.bt-us-tabs button svg{flex:none;width:13px;height:13px;margin-left:-2px} --.bt-us-pick{position:absolute;top:calc(100% + 8px);left:0;max-width:100%;box-sizing:border-box;padding:14px 16px 12px;border-radius:18px;border:.5px solid var(--bt-line-2);background:var(--bt-card);box-shadow:0 22px 50px -18px rgba(0,0,0,.32),0 2px 8px rgba(0,0,0,.05);transform-origin:24px 0;animation:bt-us-drop .2s var(--bt-us-ease)} --.bt-us-pick-quick{display:flex;gap:6px;margin-bottom:12px} --.bt-us-pick-quick button{height:26px;padding:0 11px;border:0;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease} --.bt-us-pick-quick button:hover{background:var(--bt-active);color:var(--bt-ink)} --.bt-us-pick-quick button[aria-pressed=true]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:var(--bt-accent)} --.bt-us-pick-months{display:flex;flex-wrap:wrap;gap:8px 28px} --.bt-us-month{width:252px;animation:bt-us-fade .22s ease} --.bt-us-month-head{position:relative;display:flex;align-items:center;justify-content:center;height:28px;margin-bottom:4px;font-size:13px;line-height:18px;font-weight:600} --.bt-us-month-nav{position:absolute;top:0;width:28px;height:28px;padding:0;border:0;border-radius:9px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease} --.bt-us-month-nav[data-side=left]{left:0} --.bt-us-month-nav[data-side=right]{right:0} --.bt-us-month-nav:hover:not(:disabled){background:var(--bt-hover);color:var(--bt-ink)} --.bt-us-month-nav:disabled{opacity:.3;cursor:default} --.bt-us-weekdays,.bt-us-days{display:grid;grid-template-columns:repeat(7,36px)} --.bt-us-weekdays span{height:24px;font-size:11px;line-height:24px;text-align:center;color:var(--bt-ink-3)} --.bt-us-day{position:relative;height:36px;padding:0;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:12px;font-variant-numeric:tabular-nums;cursor:pointer} --.bt-us-day::before{content:'';position:absolute;top:4px;bottom:4px;left:0;right:0;transition:background-color .12s ease} --.bt-us-day>b{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;font-weight:400;transition:background-color .12s ease,color .12s ease} --.bt-us-day:hover:not(:disabled)>b{background:var(--bt-hover)} --.bt-us-day[data-today]>b{color:var(--bt-accent);font-weight:600} --.bt-us-day[data-has]::after{content:'';position:absolute;z-index:2;left:50%;bottom:6px;width:3px;height:3px;margin-left:-1.5px;border-radius:50%;background:var(--bt-ink-3)} --.bt-us-day[data-in]::before{background:color-mix(in srgb,var(--bt-accent) 13%,transparent)} --.bt-us-day[data-in][data-l]::before{left:4px;border-radius:14px 0 0 14px} --.bt-us-day[data-in][data-r]::before{right:4px;border-radius:0 14px 14px 0} --.bt-us-day[data-in][data-l][data-r]::before{border-radius:14px} --.bt-us-day[data-in][data-start]::before{left:50%;border-radius:0} --.bt-us-day[data-in][data-end]::before{right:50%;border-radius:0} --.bt-us-day[data-in][data-start][data-r]::before,.bt-us-day[data-in][data-end][data-l]::before,.bt-us-day[data-in][data-start][data-end]::before{display:none} --.bt-us-day[data-start]>b,.bt-us-day[data-end]>b,.bt-us-day[data-start]:hover>b,.bt-us-day[data-end]:hover>b{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} --.bt-us-day[data-start][data-has]::after,.bt-us-day[data-end][data-has]::after{background:color-mix(in srgb,var(--bt-accent-ink,#fff) 75%,transparent)} --.bt-us-day:disabled{color:var(--bt-ink-3);opacity:.45;cursor:default} --.bt-us-pick-foot{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:8px;padding-top:10px;border-top:1px solid var(--bt-line);font-size:12px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap} --.bt-us-pick-foot b{color:var(--bt-ink);font-weight:500} --.bt-us-tabs button:hover,.bt-us-tabs button[aria-pressed=true]{color:var(--bt-ink)} --.bt-us-tabs:not([data-glide]) button[aria-pressed=true]{background:var(--bt-card)} --.bt-us-tabs>.bt-thumb{top:3px;bottom:3px;border-radius:999px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08),0 0 0 .5px var(--bt-line-2)} --.bt-us-note{display:inline-flex;align-items:center;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-3)} --.bt-us-note::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--bt-accent);animation:bt-us-pulse 1.4s ease-in-out infinite} --.bt-us-sheet{display:flex;flex-direction:column;gap:4px} --.bt-us-sheet>.bt-us-stats,.bt-us-sheet>.bt-us-card,.bt-us-sheet>.bt-us-pair>.bt-us-card{border-radius:0} --.bt-us-sheet>:first-child{border-radius:16px 16px 0 0} --.bt-us-sheet>:last-child{border-radius:0 0 16px 16px} --.bt-us-stats{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;border-radius:16px;border:1px solid var(--bt-line);background:var(--bt-card);animation:bt-us-in .45s var(--bt-us-ease) backwards} --.bt-us-stat{display:flex;flex-direction:column;justify-content:flex-end;gap:6px;min-width:0;padding:16px 18px} --.bt-us-stat+.bt-us-stat{border-left:1px solid var(--bt-line)} --.bt-us-stat-label{overflow:hidden;font-size:12px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap;text-overflow:ellipsis} --.bt-us-stat-value{display:flex;align-items:baseline;gap:6px;min-width:0;font-size:22px;line-height:28px;font-weight:600;letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap} --.bt-us-stat-main .bt-us-stat-value{font-size:34px;line-height:40px;letter-spacing:-.025em} --.bt-us-stat-value small{font-size:13px;font-weight:400;letter-spacing:0;color:var(--bt-ink-3)} --.bt-us-delta{align-self:center;margin-left:2px;padding:0 8px;border-radius:999px;background:var(--bt-tag-bg);color:var(--bt-ink-2);font-size:11px;line-height:20px;font-weight:500;letter-spacing:0} --.bt-us-card{display:flex;flex-direction:column;gap:12px;min-width:0;padding:14px 18px 16px;border-radius:16px;border:1px solid var(--bt-line);background:var(--bt-card);animation:bt-us-in .45s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 45ms)} --.bt-us-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:32px} --.bt-us-card-head h3{display:flex;align-items:baseline;gap:8px;min-width:0;margin:0;font-size:13px;line-height:18px;font-weight:600} --.bt-us-card-head h3 span,.bt-us-head-note{font-size:12px;font-weight:400;color:var(--bt-ink-3)} --.bt-us-chart-body{display:flex;flex-direction:column;gap:8px;padding-top:6px} --.bt-us-plot{position:relative;height:200px;margin-left:40px} --.bt-us-grid{position:absolute;left:0;right:0;border-top:1px dashed var(--bt-line-2);pointer-events:none} --.bt-us-grid.bt-us-base{border-top:1px solid var(--bt-line-2)} --.bt-us-grid span{position:absolute;right:calc(100% + 8px);top:-8px;font-size:10px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} --.bt-us-cols{position:absolute;inset:0;display:flex;align-items:flex-end} --.bt-us-col{position:relative;flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end;justify-content:center} --.bt-us-stack{display:flex;flex-direction:column-reverse;gap:2px;width:56%;min-width:4px;max-width:28px;transform-origin:50% 100%;animation:bt-us-grow .6s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 14ms);transition:height .45s var(--bt-us-ease),opacity .18s ease} --.bt-us-stack>i{flex:1 1 0;min-height:2px;border-radius:5px;transition:flex-grow .45s var(--bt-us-ease)} --.bt-us-plot[data-hover] .bt-us-col:not([data-on]) .bt-us-stack{opacity:.3} --.bt-us-stub{width:4px;height:4px;margin-bottom:-2px;border-radius:50%;background:var(--bt-line-2)} --.bt-us-ticks{display:flex;margin-left:40px;font-size:11px;line-height:14px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums} --.bt-us-ticks span{flex:1 1 0;min-width:0;text-align:center;white-space:nowrap} --.bt-us-ticks span[data-now]{color:var(--bt-ink);font-weight:500} --.bt-us-tip{position:absolute;top:6px;z-index:3;min-width:160px;max-width:240px;padding:10px 12px;border-radius:12px;border:.5px solid var(--bt-line-2);background:color-mix(in srgb,var(--bt-card) 90%,transparent);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);box-shadow:0 14px 34px -12px rgba(0,0,0,.28);font-size:12px;line-height:16px;pointer-events:none;animation:bt-us-pop .16s var(--bt-us-ease)} --.bt-us-tip-title{color:var(--bt-ink-3)} --.bt-us-tip-total{margin:2px 0 8px;font-size:15px;line-height:20px;font-weight:600;font-variant-numeric:tabular-nums} --.bt-us-tip-total:last-child{margin-bottom:0} --.bt-us-tip-row{display:flex;align-items:center;gap:8px;color:var(--bt-ink-2)} --.bt-us-tip-row+.bt-us-tip-row{margin-top:5px} --.bt-us-tip-row span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-us-tip-row b{font-weight:500;color:var(--bt-ink);font-variant-numeric:tabular-nums} --.bt-us-legend{display:flex;flex-wrap:wrap;gap:6px} --.bt-us-key{display:inline-flex;align-items:center;gap:6px;min-width:0;height:24px;padding:0 10px 0 8px;border:0;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2);font:inherit;font-size:12px;line-height:24px} --button.bt-us-key{cursor:pointer;transition:background-color .12s ease,color .12s ease} --button.bt-us-key:hover{background:var(--bt-active);color:var(--bt-ink)} --.bt-us-dot{flex:none;width:8px;height:8px;border-radius:50%} --.bt-us-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:4px} --.bt-us-rows{display:flex;flex-direction:column;margin:0 -8px} --.bt-us-row{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:8px;border:0;border-radius:12px;background:none;color:var(--bt-ink);font:inherit;text-align:left;animation:bt-us-in .4s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 35ms + 90ms)} --button.bt-us-row{cursor:pointer;transition:background-color .12s ease} --button.bt-us-row:hover{background:var(--bt-hover)} --.bt-us-row-glyph{flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px} --.bt-us-row-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:7px} --.bt-us-row-line{display:flex;align-items:baseline;gap:8px;min-width:0;font-size:13px;line-height:18px} --.bt-us-row-name{flex:0 1 auto;min-width:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;white-space:nowrap} --.bt-us-row-sub{flex:0 1 auto;min-width:0;overflow:hidden;font-size:12px;color:var(--bt-ink-3);text-overflow:ellipsis;white-space:nowrap} --.bt-us-track{display:block;height:4px;border-radius:999px;background:var(--bt-hover);overflow:hidden} --.bt-us-track>span{display:block;height:100%;border-radius:inherit;transform-origin:0 50%;animation:bt-us-fill .7s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 35ms + 140ms);transition:width .45s var(--bt-us-ease)} --.bt-us-row-num{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:1px;min-width:52px;font-variant-numeric:tabular-nums} --.bt-us-row-num b{font-size:13px;line-height:18px;font-weight:600} --.bt-us-row-num small{font-size:11px;line-height:14px;color:var(--bt-ink-3)} --.bt-us-split{height:1px;margin:6px 8px;background:var(--bt-line)} --.bt-us-none{padding:18px 8px;font-size:12px;color:var(--bt-ink-3);text-align:center} --.bt-us-glyph{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2)} --.bt-us-model{display:inline-flex;align-items:center;justify-content:center;border-radius:30%;font-weight:600;line-height:1} --.bt-us-profile{display:flex;align-items:center;gap:20px;padding:6px 4px 2px;animation:bt-us-in .45s var(--bt-us-ease) backwards} --.bt-us-ring{position:relative;flex:none;width:96px;height:96px} --.bt-us-ring>svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)} --.bt-us-ring>svg circle{fill:none;stroke-width:4} --.bt-us-ring-track{stroke:var(--bt-line)} --.bt-us-ring-arc{stroke-linecap:round;transition:stroke-dasharray .6s var(--bt-us-ease);animation:bt-us-ring 1s var(--bt-us-ease) .1s backwards} --.bt-us-ring-face{position:absolute;inset:0;display:flex;align-items:center;justify-content:center} --.bt-us-profile-copy{display:flex;flex-direction:column;gap:3px;min-width:0} --.bt-us-profile-copy h3{margin:0;overflow:hidden;font-size:22px;line-height:28px;font-weight:600;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap} --.bt-us-profile-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-us-profile-share{font-size:12px;line-height:16px;color:var(--bt-ink-3)} --.bt-us-heat{gap:8px} --.bt-us-heat-months,.bt-us-heat-grid{display:grid;grid-template-columns:repeat(${HEAT_WEEKS},1fr);gap:3px} --.bt-us-heat-months{height:14px;font-size:10px;line-height:14px;color:var(--bt-ink-3)} --.bt-us-heat-months span{white-space:nowrap} --.bt-us-heat-plot{position:relative} --.bt-us-heat-grid{grid-template-rows:repeat(7,auto);grid-auto-flow:column} --.bt-us-heat-grid .bt-us-cell[data-on]{outline:1.5px solid var(--bt-ink-2);outline-offset:1px} --.bt-us-heat-tip{min-width:0;white-space:nowrap} --.bt-us-cell{position:relative;aspect-ratio:1;border-radius:3px;background:var(--bt-hover);overflow:hidden} --.bt-us-heat-grid .bt-us-cell{animation:bt-us-cell .5s var(--bt-us-ease) backwards;animation-delay:calc(var(--w,0) * 7ms + 120ms)} --.bt-us-cell>i{position:absolute;inset:0} --.bt-us-cell[data-today]{outline:1.5px solid var(--bt-ink-3);outline-offset:1px} --.bt-us-cell[data-future]{visibility:hidden} --.bt-us-heat-scale{display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} --.bt-us-heat-scale .bt-us-cell{width:10px} --.bt-us-skel{border-radius:16px;background:linear-gradient(90deg,var(--bt-hover) 0%,var(--bt-active) 50%,var(--bt-hover) 100%);background-size:200% 100%;animation:bt-us-shine 1.4s ease-in-out infinite} --.bt-us-skel-sheet>.bt-us-skel:first-child{border-radius:16px 16px 0 0} --.bt-us-skel-sheet>.bt-us-skel:not(:first-child){border-radius:0} --.bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:first-child{border-radius:0 0 0 16px} --.bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:last-child{border-radius:0 0 16px 0} --.bt-us-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:72px 16px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)} --.bt-us-empty b{margin-top:6px;font-size:15px;font-weight:600;color:var(--bt-ink)} --@keyframes bt-us-in{from{opacity:0;transform:translateY(6px)}} --@keyframes bt-us-grow{from{transform:scaleY(0)}} --@keyframes bt-us-fill{from{transform:scaleX(0)}} --@keyframes bt-us-pop{from{opacity:0;transform:translateY(3px) scale(.97)}} --@keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} --@keyframes bt-us-fade{from{opacity:0}} --@keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} --@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING2)}}} --@keyframes bt-us-pulse{50%{opacity:.25}} --@keyframes bt-us-shine{from{background-position:200% 0}to{background-position:-200% 0}} --@media (prefers-reduced-motion:reduce){.bt-us *,.bt-us *::before{animation:none!important;transition:none!important}} --`; - --// src/client/connectors.js --var import_react38 = require("react"); --var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; --var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; --var FEISHU_WEBHOOK_URL = "https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot"; --var APPS = [ -- ["gmail", "Gmail"], -- ["googlecalendar", "Google Calendar"], -- ["googledrive", "Google Drive"], -- ["googledocs", "Google Docs"], -- ["googlesheets", "Google Sheets"], -- ["outlook", "Outlook"], -- ["slack", "Slack"], -- ["microsoft_teams", "Microsoft Teams"], -- ["discord", "Discord"], -- ["telegram", "Telegram"], -- ["whatsapp", "WhatsApp"], -- ["notion", "Notion"], -- ["linear", "Linear"], -- ["jira", "Jira"], -- ["asana", "Asana"], -- ["trello", "Trello"], -- ["todoist", "Todoist"], -- ["airtable", "Airtable"], -- ["dropbox", "Dropbox"], -- ["hubspot", "HubSpot"], -- ["salesforce", "Salesforce"], -- ["stripe", "Stripe"], -- ["zoom", "Zoom"], -- ["calendly", "Calendly"], -- ["figma", "Figma"], -- ["twitter", "X (Twitter)"], -- ["youtube", "YouTube"] --]; --var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; --var GitHubMark = () => (0, import_react38.createElement)( -- "svg", -- { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, -- (0, import_react38.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) --); --var FeishuMark = () => (0, import_react38.createElement)( -- "svg", -- { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react38.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }), -- (0, import_react38.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }), -- (0, import_react38.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" }) --); --var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark }; --var ABOUT = { -- github: () => t("Repositories, issues, and pull requests"), -- composio: () => t("Gmail, Slack, Calendar, Notion and many more apps, through one Composio key"), -- feishu: () => t("Posts into one Feishu group through a custom bot webhook") --}; --function statusText2(connector) { -- if (connector.state === "connecting") return t("Connecting…"); -- if (connector.state === "error") return t("Could not connect: {error}", { error: connector.error ?? "" }); -- if (connector.state === "ready" && connector.kind === "channel") { -- return connector.mode === "app" ? t("Connected: app, over the long connection") : t("Connected: custom bot webhook"); -+// src/client/exchange.js -+var import_react35 = require("react"); -+var SHEET_EXIT_MS = 220; -+function exchangeText(text2) { -+ if (typeof text2 !== "string") return ""; -+ const CLOSE = ""; -+ let body = text2; -+ for (let previous; previous !== body; ) { -+ previous = body; -+ const open = body.indexOf(" open ? body.slice(body.indexOf(">", open) + 1, close) : body; -+ body = stripFence(body.slice(0, open) + inner + (close > open ? body.slice(close + CLOSE.length) : "")); - } -- if (connector.state === "ready" && connector.apps?.length > 0) return t("Connected: {apps}", { apps: connector.apps.map(appName).join(", ") }); -- if (connector.state === "ready") return connector.account ? t("Connected as {account}", { account: connector.account }) : t("Connected"); -- return ABOUT[connector.id]?.() ?? ""; -+ return stripFence(stripTrace(body.replace(/^\s+/, ""))).trim(); - } --function CredentialField({ credential, value, onChange, onEnter, autoFocus }) { -- const label = t(credential.label); -- if (credential.masked) return (0, import_react38.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus }); -- return (0, import_react38.createElement)( -- "label", -- { className: "bt-q-field" }, -- (0, import_react38.createElement)("input", { -- className: "bt-input", -- type: "text", -- value, -- "aria-label": label, -- placeholder: label, -- spellCheck: false, -- autoFocus, -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter") onEnter?.(); -- } -- }) -+function ExchangeLine({ entry, roster, actions }) { -+ const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; -+ return (0, import_react35.createElement)( -+ "div", -+ { className: "bt-group-msg" }, -+ (0, import_react35.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), -+ (0, import_react35.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react35.createElement)("span", { className: "bt-lead" }, (0, import_react35.createElement)(BotMark, { bot, size: 22 })) }) - ); - } --function AllowedSenders({ connector, actions, run, busy }) { -- const [openId, setOpenId] = (0, import_react38.useState)(""); -- const allowed = connector.allowed ?? []; -- const refused = connector.inbound?.refusedSenders ?? []; -- const allow = (id) => void run(actions.connectorAllow(connector.id, id)); -- return (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-allowed" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")), -- allowed.length === 0 && refused.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null, -- allowed.map((entry) => (0, import_react38.createElement)( -- "div", -- { key: entry.openId, className: "bt-connector-person" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block")) -- )), -- refused.map((entry) => (0, import_react38.createElement)( -- "div", -- { key: entry.openId, className: "bt-connector-person" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow")) -- )), -- (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-form-row" }, -- (0, import_react38.createElement)("input", { -- className: "bt-input", -- type: "text", -- value: openId, -- spellCheck: false, -- "aria-label": t("Feishu open_id"), -- placeholder: t("Paste an open_id to allow"), -- onChange: (event) => setOpenId(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter" && openId.trim() !== "") { -- allow(openId.trim()); -- setOpenId(""); -- } -- } -- }), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => { -- allow(openId.trim()); -- setOpenId(""); -- } }, t("Allow")) -+function ExchangeDialog({ pair, roster, actions }) { -+ const [entries2, setEntries] = (0, import_react35.useState)(null); -+ const [leaving, setLeaving] = (0, import_react35.useState)(false); -+ const close = () => { -+ if (leaving) return; -+ setLeaving(true); -+ setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS); -+ }; -+ useEscape(close); -+ (0, import_react35.useEffect)(() => { -+ let stop = false; -+ let timer; -+ let seen = null; -+ const load = async () => { -+ const result = await actions.exchange(pair[0], pair[1]); -+ if (stop) return; -+ const key = JSON.stringify(result ?? []); -+ if (key !== seen) { -+ seen = key; -+ setEntries(result ?? []); -+ } -+ timer = setTimeout(load, document.hidden ? 8e3 : 2e3); -+ }; -+ void load(); -+ return () => { -+ stop = true; -+ clearTimeout(timer); -+ }; -+ }, [pair[0], pair[1]]); -+ const left = roster.byId[pair[0]]; -+ const right = roster.byId[pair[1]]; -+ let lastDay = 0; -+ const paneLeft = usePaneLeft(); -+ return (0, import_react35.createElement)( -+ "div", -+ { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, -+ (0, import_react35.createElement)( -+ "div", -+ { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, -+ (0, import_react35.createElement)("div", { className: "bt-head" }, (0, import_react35.createElement)( -+ "span", -+ { className: "bt-pill", style: { cursor: "default" } }, -+ left ? (0, import_react35.createElement)(BotMark, { bot: left, size: 22 }) : null, -+ left?.name ?? "?", -+ (0, import_react35.createElement)("span", { className: "bt-swap" }, "⇄"), -+ right ? (0, import_react35.createElement)(BotMark, { bot: right, size: 22 }) : null, -+ right?.name ?? "?" -+ )), -+ (0, import_react35.createElement)( -+ "div", -+ { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, -+ entries2 === null ? (0, import_react35.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { -+ const showDay = entry.time - lastDay > 30 * 60 * 1e3; -+ lastDay = entry.time; -+ return (0, import_react35.createElement)( -+ import_react35.Fragment, -+ { key: entry.id }, -+ showDay ? (0, import_react35.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, -+ (0, import_react35.createElement)(ExchangeLine, { entry, roster, actions }) -+ ); -+ }) -+ ), -+ (0, import_react35.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react35.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) - ) - ); - } --var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; --function AppChip({ name, status, onRemove }) { -- const label = APP_STATUS[status]?.(); -- return (0, import_react38.createElement)( -- "button", -- { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, -- name, -- label ? (0, import_react38.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null -- ); --} --function AppPicker({ apps, onChange, search, statuses = {} }) { -- const [query, setQuery] = (0, import_react38.useState)(""); -- const [found, setFound] = (0, import_react38.useState)(null); -- const [failed, setFailed] = (0, import_react38.useState)(null); -- const names = (0, import_react38.useRef)(new Map(APPS)); -- const asked = (0, import_react38.useRef)(0); -- (0, import_react38.useEffect)(() => { -- const text2 = query.trim(); -- if (text2 === "" || search === void 0) { -- setFound(null); -- setFailed(null); -- return void 0; -- } -- const ask = ++asked.current; -- const timer = setTimeout(() => { -- search(text2).then((list) => { -- if (ask !== asked.current) return; -- for (const app of list) names.current.set(app.slug, app.name); -- setFound(list); -- setFailed(null); -- }, (failure) => { -- if (ask !== asked.current) return; -- setFound(null); -- setFailed(failure?.message ?? String(failure)); -+ -+// src/client/voice.js -+var import_react36 = require("react"); -+var VOICE_LABELS = { -+ listening: "Listening", -+ thinking: "Thinking", -+ speaking: "Speaking", -+ paused: "Paused", -+ setup: "Voice input needs its speech model", -+ unsupported: "Voice chat is not available in this browser" -+}; -+var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; -+function useMicLevel(enabled) { -+ const [level, setLevel] = (0, import_react36.useState)(0); -+ (0, import_react36.useEffect)(() => { -+ if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; -+ let stream = null; -+ let audio = null; -+ let frame = 0; -+ let stopped = false; -+ void navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => { -+ if (stopped) { -+ media.getTracks().forEach((track) => track.stop()); -+ return; -+ } -+ stream = media; -+ audio = new AudioContext(); -+ const analyser = audio.createAnalyser(); -+ analyser.fftSize = 256; -+ audio.createMediaStreamSource(media).connect(analyser); -+ const data = new Uint8Array(analyser.frequencyBinCount); -+ let smooth = 0; -+ const tick = () => { -+ analyser.getByteTimeDomainData(data); -+ let sum = 0; -+ for (const value of data) sum += ((value - 128) / 128) ** 2; -+ smooth = smooth * 0.8 + Math.min(1, Math.sqrt(sum / data.length) * 4) * 0.2; -+ setLevel(Math.round(smooth * 100) / 100); -+ frame = requestAnimationFrame(tick); -+ }; -+ tick(); -+ }).catch(() => { -+ }); -+ return () => { -+ stopped = true; -+ cancelAnimationFrame(frame); -+ stream?.getTracks().forEach((track) => track.stop()); -+ void audio?.close().catch(() => { - }); -- }, 250); -- return () => clearTimeout(timer); -- }, [query, search]); -- const nameOf = (slug) => names.current.get(slug) ?? appName(slug); -- const picked = new Set(apps); -- const needle = query.trim().toLowerCase(); -- const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); -- const results = (found ?? local).filter((app) => !picked.has(app.slug)); -- return (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-apps" }, -- apps.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react38.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, -- (0, import_react38.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), -- failed ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, -- (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react38.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { -- names.current.set(app.slug, app.name); -- onChange([...apps, app.slug]); -- } }, app.name))) -- ); --} --function AppStatusList({ apps, statuses }) { -- if (apps.length === 0 || Object.keys(statuses).length === 0) return null; -- return (0, import_react38.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react38.createElement)( -- "li", -- { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), -- (0, import_react38.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") -- ))); -+ setLevel(0); -+ }; -+ }, [enabled]); -+ return level; - } --function ConnectorRow({ connector, roster, actions }) { -- const [form, setForm] = (0, import_react38.useState)(false); -- const [bots, setBots] = (0, import_react38.useState)(false); -- const [token, setToken] = (0, import_react38.useState)(""); -- const [apps, setApps] = (0, import_react38.useState)(() => connector.apps ?? []); -- const [picking, setPicking] = (0, import_react38.useState)(false); -- const [busy, setBusy] = (0, import_react38.useState)(false); -- const [error, setError] = (0, import_react38.useState)(null); -- const [message, setMessage] = (0, import_react38.useState)(""); -- const run = (task, after) => { -- setBusy(true); -- setError(null); -- return task.then(() => { -- after?.(); -- }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -+function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }) { -+ const bot = roster.byId[sessionId]; -+ const room2 = roster.roomsById[sessionId]; -+ const name = bot?.name ?? room2?.name ?? t("Bot"); -+ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -+ const lastReply = useSessions((list) => { -+ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -+ const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; -+ return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; -+ }); -+ const [speech] = (0, import_react36.useState)(() => actions.speech()); -+ const [phase, setPhase] = (0, import_react36.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); -+ const [catalog, setCatalog] = (0, import_react36.useState)(null); -+ const [heard, setHeard] = (0, import_react36.useState)(""); -+ const [said, setSaid] = (0, import_react36.useState)(""); -+ const [note, setNote] = (0, import_react36.useState)(""); -+ const [serviceLevel, setServiceLevel] = (0, import_react36.useState)(0); -+ const [cycle, setCycle] = (0, import_react36.useState)(0); -+ const waiting = (0, import_react36.useRef)(null); -+ const reply = (0, import_react36.useRef)(lastReply); -+ reply.current = lastReply; -+ const turn = (0, import_react36.useRef)(0); -+ const paneLeft = usePaneLeft(); -+ const close = () => { -+ turn.current += 1; -+ window.speechSynthesis?.cancel(); -+ actions.closeVoice(); - }; -- const gateway = connector.id === "composio"; -- const sender = connector.kind === "sender"; -- const channel = connector.kind === "channel"; -- const modes = connector.modes ?? []; -- const [mode, setMode] = (0, import_react38.useState)(() => connector.mode ?? modes[0]?.id); -- const [values, setValues] = (0, import_react38.useState)({}); -- const chosen = modes.find((entry) => entry.id === mode) ?? modes[0]; -- const field = (name) => values[name] ?? ""; -- const setField = (name, value) => setValues((current) => ({ ...current, [name]: value })); -- const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== ""); -- const [statuses, setStatuses] = (0, import_react38.useState)({}); -- const appsKey = (connector.apps ?? []).join(","); -- (0, import_react38.useEffect)(() => { -- if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { -- setStatuses({}); -- return void 0; -- } -+ useEscape(close); -+ const submit = (text2) => { -+ setHeard(text2); -+ setPhase("thinking"); -+ waiting.current = reply.current.split("|")[0]; -+ void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); -+ }; -+ const dictation = useDictation((text2, final) => { -+ setHeard(text2); -+ if (!final) return; -+ dictation.stop(); -+ submit(text2); -+ }); -+ (0, import_react36.useEffect)(() => { -+ if (!note) return void 0; -+ const timer = setTimeout(() => setNote(""), 2600); -+ return () => clearTimeout(timer); -+ }, [note]); -+ (0, import_react36.useEffect)(() => { -+ if (!speech) return void 0; - let live = true; -- actions.connectorAppStatus(connector.id).then((value) => { -- if (live) setStatuses(value ?? {}); -+ speechCatalog(speech).then(({ catalog: next, usable }) => { -+ if (!live) return; -+ setCatalog(next); -+ setPhase(usable ? "listening" : "setup"); - }, () => { -- if (live) setStatuses({}); -+ if (live) setPhase("setup"); - }); - return () => { - live = false; - }; -- }, [gateway, connector.state, appsKey, picking]); -- const search = (0, import_react38.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); -- const connect = () => { -- if (busy) return; -- if (channel) { -- if (chosen === void 0 || !filled) return; -- const credentials = Object.fromEntries(chosen.credentials.map((credential) => [credential.name, field(credential.name)])); -- void run(actions.connectorConnect(connector.id, { mode: chosen.id, credentials }), () => { -- setValues({}); -- setForm(false); -- }); -- return; -- } -- if (token.trim() === "") return; -- if (gateway && apps.length === 0) { -- setError(t("Pick at least one app")); -- return; -- } -- void run(actions.connectorConnect(connector.id, token, gateway ? apps : void 0), () => { -- setToken(""); -- setForm(false); -- }); -- }; -- const saveApps = () => { -- if (busy) return; -- if (apps.length === 0) { -- setError(t("Pick at least one app")); -+ }, [speech]); -+ const listen = phase === "listening"; -+ (0, import_react36.useEffect)(() => { -+ if (speech || !listen || dictation.listening) return void 0; -+ const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { -+ } }), 250); -+ return () => clearTimeout(timer); -+ }, [listen, dictation.listening]); -+ (0, import_react36.useEffect)(() => { -+ if (phase !== "listening") dictation.stop(); -+ }, [phase]); -+ (0, import_react36.useEffect)(() => { -+ if (!speech || !catalog || !listen) return void 0; -+ const own = turn.current; -+ let capture = null; -+ let frame = 0; -+ let stopped = false; -+ let smooth = 0; -+ const detect = turnDetector(); -+ const limit = Math.max(1, catalog.maxDurationSeconds - 1) * 1e3; -+ const finish = async (current) => { -+ setPhase("transcribing"); -+ try { -+ const wave = await current.finish(catalog.maxDurationSeconds); -+ const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; -+ if (turn.current !== own) return; -+ if (text2) submit(text2); -+ else setPhase("listening"); -+ } catch (error) { -+ if (turn.current !== own) return; -+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that")); -+ setPhase("listening"); -+ } -+ }; -+ openCapture().then((opened) => { -+ if (stopped) { -+ opened.cancel(); -+ return; -+ } -+ capture = opened; -+ const began = performance.now(); -+ const tick = () => { -+ const now = performance.now(); -+ const level2 = capture.level(); -+ smooth = smooth * 0.8 + level2 * 0.2; -+ setServiceLevel(Math.round(smooth * 100) / 100); -+ const state = detect(level2, now); -+ if (state === "done" || state === "speech" && now - began >= limit) { -+ const current = capture; -+ capture = null; -+ setServiceLevel(0); -+ void finish(current); -+ return; -+ } -+ if (state === "waiting" && now - began >= limit) { -+ setCycle((value) => value + 1); -+ return; -+ } -+ frame = requestAnimationFrame(tick); -+ }; -+ tick(); -+ }, (error) => { -+ if (stopped) return; -+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that")); -+ setPhase("paused"); -+ }); -+ return () => { -+ stopped = true; -+ cancelAnimationFrame(frame); -+ capture?.cancel(); -+ setServiceLevel(0); -+ }; -+ }, [speech, catalog, listen, cycle]); -+ (0, import_react36.useEffect)(() => { -+ if (phase !== "thinking" || running || waiting.current === null) return; -+ const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; -+ if (count === waiting.current || !text2.trim()) return; -+ waiting.current = null; -+ const spoken = speechOf(text2, botNames(roster)); -+ setSaid(spoken); -+ setHeard(""); -+ if (!window.speechSynthesis || !spoken) { -+ setPhase("listening"); - return; - } -- void run(actions.connectorApps(connector.id, apps), () => setPicking(false)); -+ const utterance = new SpeechSynthesisUtterance(spoken); -+ utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US"; -+ utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current); -+ utterance.onerror = utterance.onend; -+ setPhase("speaking"); -+ window.speechSynthesis.cancel(); -+ window.speechSynthesis.speak(utterance); -+ }, [phase, running, lastReply]); -+ (0, import_react36.useEffect)(() => () => window.speechSynthesis?.cancel(), []); -+ const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); -+ const level = speech ? serviceLevel : micLevel; -+ const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -+ const blocked = phase === "unsupported" || phase === "setup" || phase === "starting"; -+ const togglePause = () => { -+ if (phase === "paused") { -+ setPhase("listening"); -+ return; -+ } -+ turn.current += 1; -+ window.speechSynthesis?.cancel(); -+ setPhase("paused"); - }; -- const on = connector.state !== "off"; -- const canPost = sender || channel && connector.mode === "webhook"; -- const perBot = !sender && (!channel || connector.mode === "app"); -- const Mark = MARKS[connector.id]; -- const row = (0, import_react38.useRef)(null); -- useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`); -- return (0, import_react38.createElement)( -+ const openSetup = phase === "setup" ? actions.voiceSetup() : null; -+ const caption = phase === "speaking" ? said : heard; -+ const shownNote = note || dictation.note; -+ return (0, import_react36.createElement)( - "div", -- { ref: row, className: "bt-connector", "data-state": connector.state }, -- (0, import_react38.createElement)( -+ { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, -+ (0, import_react36.createElement)( - "div", -- { className: "bt-connector-head" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react38.createElement)(Mark) : null), -- (0, import_react38.createElement)( -- "span", -- { className: "bt-connector-text" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-name" }, connector.name), -- (0, import_react38.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) -+ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-voice-orb", style: { "--bt-level": level } }, -+ (0, import_react36.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), -+ (0, import_react36.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), -+ bot ? (0, import_react36.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react36.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null - ), -- (0, import_react38.createElement)( -- "span", -- { className: "bt-connector-actions" }, -- connector.state === "error" ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, -- on && gateway && !picking ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -- setApps(connector.apps ?? []); -- setPicking(true); -- } }, t("Choose apps")) : null, -- on && perBot ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, -- on ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) -- ) -- ), -- on && gateway && !picking ? (0, import_react38.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, -- on && perBot ? (0, import_react38.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, -- on && bots ? (0, import_react38.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, -- on && channel && connector.mode === "app" ? (0, import_react38.createElement)(AllowedSenders, { connector, actions, run, busy }) : null, -- on && canPost ? (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-form" }, -- (0, import_react38.createElement)("label", { className: "bt-q-field" }, (0, import_react38.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })), -- (0, import_react38.createElement)( -+ (0, import_react36.createElement)("div", { className: "bt-voice-name" }, name), -+ (0, import_react36.createElement)( - "div", -- { className: "bt-connector-form-row" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-link" }), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send")) -- ) -- ) : null, -- !on && form ? (0, import_react38.createElement)( -+ { className: "bt-voice-status", role: "status" }, -+ phase === "thinking" ? (0, import_react36.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react36.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" -+ ), -+ (0, import_react36.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), -+ openSetup ? (0, import_react36.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { -+ close(); -+ openSetup(); -+ } }, t("Set up voice input")) : null, -+ shownNote ? (0, import_react36.createElement)("div", { className: "bt-voice-note" }, shownNote) : null -+ ), -+ (0, import_react36.createElement)( - "div", -- { className: "bt-connector-form" }, -- channel ? (0, import_react38.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react38.createElement)("button", { -- key: entry.id, -+ { className: "bt-voice-bar" }, -+ (0, import_react36.createElement)("button", { - type: "button", -- className: "bt-state", -- "aria-pressed": entry.id === chosen?.id, -- onClick: () => { -- setMode(entry.id); -- setValues({}); -- setError(null); -- } -- }, t(entry.label)))) : null, -- channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react38.createElement)(CredentialField, { -- key: credential.name, -- credential, -- value: field(credential.name), -- onChange: (value) => setField(credential.name, value), -- onEnter: connect, -- autoFocus: index === 0 -- })) : (0, import_react38.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react38.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), -- gateway ? (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, -- (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-form-row" }, -- (0, import_react38.createElement)( -- "a", -- { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, -- channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub") -- ), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -- setForm(false); -- setToken(""); -- setValues({}); -- setError(null); -- } }, t("Cancel")), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) -- ) -- ) : null, -- on && gateway && picking ? (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-form" }, -- (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), -- (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), -- (0, import_react38.createElement)( -- "div", -- { className: "bt-connector-form-row" }, -- (0, import_react38.createElement)("span", { className: "bt-connector-link" }), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -- setPicking(false); -- setError(null); -- } }, t("Cancel")), -- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) -- ) -- ) : null, -- error ? (0, import_react38.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null -- ); --} --function ConnectorsPage({ roster, actions }) { -- const list = roster?.connectors ?? []; -- return (0, import_react38.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react38.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react38.createElement)("div", { className: "bt-set-row" }, (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react38.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) -+ className: "bt-voice-btn", -+ "aria-pressed": phase === "paused", -+ disabled: blocked, -+ "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), -+ onClick: togglePause -+ }, (0, import_react36.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react36.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react36.createElement)(CloseIcon)) -+ ) - ); - } - --// src/client/feedback.js --var import_react39 = require("react"); --var FeedbackIcon = () => (0, import_react39.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_react39.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), -- (0, import_react39.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) --); --function download(name, text2) { -- const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); -- const anchor = document.createElement("a"); -- anchor.href = url; -- anchor.download = name; -- anchor.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); -+// src/client/workflows.js -+var import_react37 = require("react"); -+var READ_MS = 8e3; -+var POLL_MS6 = 2e3; -+var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; -+var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; -+function workflowCallMissing(failure) { -+ const code = failure && typeof failure === "object" ? String(failure.code ?? "") : ""; -+ const message = typeof failure === "string" ? failure : String(failure?.message ?? ""); -+ return code === "bot/not-found" || /Unknown FlyBot endpoint workflow\//i.test(message); - } --var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); --function whenText2(time) { -- const date = new Date(time); -- const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -- return today ? clock4 : `${date.toLocaleDateString(dateLocale(), { month: "short", day: "numeric" })} ${clock4}`; -+function workflowCallError(failure, fallback = "Could not read workflows") { -+ if (workflowCallMissing(failure)) return WORKFLOW_PLUGIN_HINT; -+ if (typeof failure === "string" && failure.trim()) return failure.trim(); -+ const message = typeof failure?.message === "string" ? failure.message.trim() : ""; -+ return message || fallback; - } --function entrySummary(entry) { -- if (entry.source === "turn") return failureLabel(entry.code, entry.text); -- return entry.text; -+function settleWorkflowRead(value, fallback = "Could not read workflows") { -+ if (!value || value.ok === false || !Array.isArray(value.workflows)) { -+ const raw = typeof value?.error === "string" && value.error.trim() ? value.error : ""; -+ const error = workflowCallMissing(value) || workflowCallMissing(raw) ? WORKFLOW_PLUGIN_HINT : raw || fallback; -+ return { phase: "error", error, pack: null }; -+ } -+ const templates = Array.isArray(value.templates) ? value.templates : []; -+ const workflows = value.workflows; -+ const pack = { ...value, templates, workflows }; -+ if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack }; -+ return { phase: "ready", error: "", pack }; - } --var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" }; --function FeedbackPage({ actions, run, Section, Row: Row4 }) { -- const [what, setWhat] = (0, import_react39.useState)(""); -- const [report, setReport] = (0, import_react39.useState)(null); -- const [preview, setPreview] = (0, import_react39.useState)(false); -- const [note, setNote] = (0, import_react39.useState)(""); -- const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { -- setReport(next); -- return next; -- }); -- const load = () => fetchReport().then(() => void 0); -- (0, import_react39.useEffect)(() => { -- void run(load()); -- }, []); -- const copyReport = () => run(fetchReport().then(async (next) => { -- const copied = await copy(next.markdown); -- setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); -- if (!copied) setPreview(true); -- })); -- const [issueLink, setIssueLink] = (0, import_react39.useState)(null); -- const openIssue = () => { -- const win = window.open("", "_blank"); -- if (win !== null) win.opener = null; -- run(fetchReport().then(async (next) => { -- const copied = await copy(next.markdown); -- const { url, inline } = issueUrl(next.issuesUrl, { what, diagnostics: next.markdown }); -- if (win !== null) { -- win.location.href = url; -- setIssueLink(null); -- setNote(inline ? t("Opened a new GitHub issue with the diagnostics filled in. Check it before you submit.") : copied ? t("Opened a new GitHub issue. The diagnostics are long, so they are on the clipboard: paste them into the issue.") : t("Opened a new GitHub issue. Copy the diagnostics below into it.")); -- } else { -- setIssueLink(url); -- setNote(t("The browser blocked the new window.")); -- } -- if (!inline && !copied) setPreview(true); -- })); -- }; -- const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { -- download("ds-bot-diagnostics.log", text2 || "No entries.\n"); -- 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.")))); -- 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); -- return [ -- (0, import_react39.createElement)( -- Section, -- { key: "report", title: t("Report a problem") }, -- (0, import_react39.createElement)( -- "div", -- { className: "bt-set-row bt-set-paste bt-fb-what" }, -- (0, import_react39.createElement)( -- "label", -- { className: "bt-set-copy", htmlFor: "bt-fb-what" }, -- (0, import_react39.createElement)("span", null, t("What happened?")), -- (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) -- ), -- (0, import_react39.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) -- ), -- (0, import_react39.createElement)( -- Row4, -- { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, -- (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), -- (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) -- ), -- preview ? (0, import_react39.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react39.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, -- (0, import_react39.createElement)( -- Row4, -- { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, -- (0, import_react39.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) -- ) -- ), -- note ? (0, import_react39.createElement)( -- "div", -- { key: "note", className: "bt-note bt-fb-note", role: "status" }, -- note, -- issueLink ? [" ", (0, import_react39.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null -- ) : null, -- (0, import_react39.createElement)( -- Section, -- { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, -- shown.length === 0 ? (0, import_react39.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react39.createElement)( -- "div", -- { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, -- (0, import_react39.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), -- (0, import_react39.createElement)( -- "div", -- { className: "bt-set-copy" }, -- (0, import_react39.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), -- (0, import_react39.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) -- ), -- entry.code ? (0, import_react39.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null -- )), -- (0, import_react39.createElement)( -- Row4, -- { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, -- (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), -- (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) -- ) -- ) -- ]; --} --var TOAST_MS = 6e3; --function ErrorToast({ useUi, actions }) { -- const toast = useUi((state) => state.toast); -- const [leaving, setLeaving] = (0, import_react39.useState)(false); -- (0, import_react39.useEffect)(() => { -- if (!toast) return void 0; -- setLeaving(false); -- const fade = setTimeout(() => setLeaving(true), TOAST_MS); -- const gone = setTimeout(() => actions.dismissToast(toast.token), TOAST_MS + 200); -- return () => { -- clearTimeout(fade); -- clearTimeout(gone); -+function readWorkflows(call4, { timeoutMs = READ_MS } = {}) { -+ return new Promise((resolve) => { -+ let settled = false; -+ const finish = (value) => { -+ if (settled) return; -+ settled = true; -+ clearTimeout(timer); -+ resolve(value); - }; -- }, [toast?.token]); -- if (!toast) return null; -- const info = toast.tone === "info"; -- return (0, import_react39.createElement)( -- "div", -- { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, -- (0, import_react39.createElement)("span", { className: "bt-toast-text" }, toast.text), -- info ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { -- actions.dismissToast(toast.token); -- actions.openSettings("feedback"); -- } }, t("Details")), -- (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react39.createElement)(CloseIcon)) -- ); -+ const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs); -+ Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null })); -+ }); - } --var FEEDBACK_CSS = ` --.bt-fb-what{cursor:default} --.bt-fb-what:hover,.bt-fb-entry:hover,.bt-fb-preview:hover{background:none} --.bt-fb-what .bt-input-area{min-height:80px} --.bt-fb-preview{cursor:default;padding:0} --.bt-fb-preview pre{margin:0;width:100%;max-height:240px;overflow:auto;padding:10px 12px;box-sizing:border-box;font:11px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--bt-ink-2);white-space:pre-wrap;word-break:break-word;outline:none} --.bt-fb-primary{height:28px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);padding:0 14px;font-size:12px;cursor:pointer;transition:filter .12s ease} --.bt-fb-primary:hover{filter:brightness(1.08)} --.bt-fb-primary:active{filter:brightness(.94)} --.bt-fb-note{margin:-8px 4px 0} --.bt-fb-entry{cursor:default;align-items:flex-start} --.bt-fb-dot{flex:none;width:6px;height:6px;border-radius:50%;margin-top:6px;background:#e02135} --.bt-fb-dot[data-level=warn]{background:#e3a008} --.bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} --.bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} --.bt-event-error{color:#e02135} --.bt-toast{position:fixed;left:0;right:0;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;width:fit-content;max-width:min(560px,calc(100vw - 32px));margin-inline:auto;padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} --.bt-toast[data-leaving]{opacity:0;transform:translateY(6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} --.bt-toast-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical} --.bt-toast-text::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#e02135;margin:0 8px 1px 0} --.bt-toast-link{flex:none;border:0;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;padding:4px 6px;border-radius:6px} --.bt-toast-link:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-toast-close{flex:none} --@keyframes bt-toast-in{from{transform:translateY(16px) scale(.94)}} --@media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}} --`; -- --// src/client/about.js --var import_react40 = require("react"); --var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); --function botHint(update) { -- if (update.phase === "installing") return t("Updating FlyBot core…"); -- if (update.phase === "installed") return t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }); -- if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") }); -- if (update.available) return t("FlyBot core {version} is available", { version: update.latest }); -- if (update.enabled === false) return t("Update checks are off."); -- if (update.source === "path") return t("Installed from a local folder; update it there."); -- if (update.source === "git" && update.build?.branch !== void 0) return branchHint(update); -- if (update.source === "git" && update.build?.pinned) return t("Pinned to commit {commit}. Updates come from the maintainer: reinstall FlyBot to get them.", { commit: update.build.commit }); -- if (!update.checkedAt) return null; -- if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install."); -- const checked = t("Checked {time}", { time: whenText2(update.checkedAt) }); -- return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`; -+function moveNode(nodes, id, x, y) { -+ return nodes.map((node) => node.id === id ? { ...node, x, y } : node); - } --function branchHint({ build, behind, checkedAt }) { -- const where = [build.branch, build.commit].filter(Boolean).join(" "); -- if (build.commit === void 0 || !checkedAt) return t("Installed from GitHub ({where}). Updates come from the maintainer: reinstall FlyBot to get them.", { where }); -- const checked = t("Checked {time}", { time: whenText2(checkedAt) }); -- const state = behind === null ? t("Could not compare with {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : behind === 0 ? t("Up to date with {branch}.", { branch: build.branch }) : behind === 1 ? t("1 commit behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : t("{count} commits behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { count: behind, branch: build.branch }); -- return `${state} · ${checked}`; -+function connectNodes(edges, from, to) { -+ if (!from || !to || from === to) return edges; -+ if (edges.some((edge) => edge.from === from && edge.to === to)) return edges; -+ return edges.concat([{ from, to }]); - } --var versionLine = (update) => [t("Version {version}", { version: update.current }), [update.build?.branch, update.build?.commit].filter(Boolean).join(" ")].filter(Boolean).join(" · "); --function dshHint(dsh, shown) { -- if (dsh.phase === "installing") return t("Updating DSH…"); -- if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }); -- if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") }); -- if (dsh.available === false && dsh.blockedBy) return t("Update FlyBot core {version} first.", { version: dsh.blockedBy }); -- if (shown) return t("DSH {version} is available", { version: dsh.verified }); -- return t("Version {version}", { version: dsh.current }); -+function dropBot(nodes, bot, x, y) { -+ const ids = new Set(nodes.map((node) => node.id)); -+ let n = nodes.length + 1; -+ while (ids.has(`n${n}`)) n += 1; -+ return nodes.concat([{ id: `n${n}`, name: bot.name, botId: bot.id, role: bot.role ?? "", x, y }]); - } --var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); --function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) { -- const update = useUpdate((value) => value) ?? {}; -- const [checking, setChecking] = (0, import_react40.useState)(false); -- const [stars, setStars] = (0, import_react40.useState)(null); -- (0, import_react40.useEffect)(() => { -- let live = true; -- actions.repoStars().then((count) => { -- if (live && Number.isInteger(count)) setStars(count); -- }, () => { -- }); -- return () => { -- live = false; -- }; -- }, []); -- const check = () => { -- setChecking(true); -- void run(actions.checkUpdate()).then(() => { -- setChecking(false); -- }); -+function removeNode(nodes, edges, id) { -+ return { -+ nodes: nodes.filter((node) => node.id !== id), -+ edges: edges.filter((edge) => edge.from !== id && edge.to !== id) - }; -- const phase = update.phase ?? "idle"; -- const dsh = update.dsh; -- const dshPhase = dsh?.phase ?? "idle"; -- const dshNew = showsDshRow(dsh, update.app); -- const dshShown = dshNew && dsh.available; -- const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react40.createElement)( -- "button", -- { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, -- phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") -- ) : (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); -- const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react40.createElement)( -- "button", -- { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, -- dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") -- ) : null; -- const repo = update.repoUrl; -- return [ -- (0, import_react40.createElement)( -- "div", -- { key: "hero", className: "bt-about-hero" }, -- (0, import_react40.createElement)("span", { className: "bt-about-mark" }, (0, import_react40.createElement)(FlyBotMark, { size: 52 })), -- (0, import_react40.createElement)( -- "div", -- { className: "bt-about-copy" }, -- (0, import_react40.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), -- update.current ? (0, import_react40.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null -- ), -- repo ? (0, import_react40.createElement)( -- "a", -- { -- className: "bt-star", -- href: repo, -- target: "_blank", -- rel: "noopener noreferrer", -- title: t("Star FlyBot on GitHub"), -- "aria-label": stars === null ? t("Star FlyBot on GitHub") : `${t("Star FlyBot on GitHub")} · ${t("{count} stars", { count: stars })}` -- }, -- (0, import_react40.createElement)("span", { className: "bt-star-main" }, (0, import_react40.createElement)("span", { className: "bt-star-icon" }, (0, import_react40.createElement)(StarLineIcon)), "Star"), -- stars === null ? null : (0, import_react40.createElement)("span", { className: "bt-star-count" }, starCount(stars)) -- ) : null -- ), -- repo ? (0, import_react40.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, -- (0, import_react40.createElement)( -- Section, -- { key: "updates", title: t("Updates") }, -- (0, import_react40.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), -- dsh?.current ? (0, import_react40.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null -- ), -- (0, import_react40.createElement)( -- Section, -- { key: "links", title: t("Links") }, -- repo ? (0, import_react40.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react40.createElement)("a", linkProps(repo), (0, import_react40.createElement)(GitHubMark), t("Open"))) : null, -- update.notesUrl ? (0, import_react40.createElement)(Row4, { label: t("Release notes") }, (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, -- (0, import_react40.createElement)(Row4, { label: t("Report a problem") }, (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) -- ) -- ]; - } --var ABOUT_CSS = ` --.bt-about-hero{display:flex;align-items:center;gap:14px;padding:16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} --.bt-about-mark{flex:none;display:inline-flex} --.bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} --.bt-about-name{font-size:16px;line-height:22px;font-weight:600} --.bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} --.bt-about-ask{margin:-10px 4px 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} --.bt-star{flex:none;display:inline-flex;align-items:stretch;height:30px;border-radius:999px;border:1px solid var(--bt-line-2,var(--bt-line));overflow:hidden;color:var(--bt-ink);text-decoration:none;font-size:12px;line-height:28px;transition:transform .12s ease,border-color .15s ease} --.bt-star:hover{border-color:color-mix(in srgb,#e3a008 55%,var(--bt-line))} --.bt-star:active{transform:scale(.96)} --.bt-star-main{display:inline-flex;align-items:center;gap:6px;padding:0 12px 0 10px;background:var(--bt-hover);font-weight:500} --.bt-star-icon{display:inline-flex;color:var(--bt-ink-2);transition:color .15s ease} --.bt-star-icon path{transition:fill .15s ease} --.bt-star:hover .bt-star-icon{color:#e3a008;animation:bt-star-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-star:hover .bt-star-icon path{fill:currentColor} --.bt-star-count{display:inline-flex;align-items:center;padding:0 11px;border-left:1px solid var(--bt-line-2,var(--bt-line));color:var(--bt-ink-2);font-variant-numeric:tabular-nums;animation:bt-star-count-in ${SPRING_POP.duration}ms ${SPRING_POP.easing},bt-veil-in .2s ease-out} --@keyframes bt-star-pop{from{scale:.6;rotate:-30deg}} --@keyframes bt-star-count-in{from{translate:-6px 0}} --.bt-about-link{display:inline-flex;align-items:center;gap:6px;text-decoration:none} --.bt-about-link svg{width:14px;height:14px} --.bt-fb-primary:disabled{opacity:.6;cursor:default;filter:none} --@media (prefers-reduced-motion:reduce){.bt-star:hover .bt-star-icon,.bt-star-count{animation:none}} --`; -- --// src/client/settings.js --var import_react41 = require("react"); --var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); --var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["projects", "Projects"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["workflows", "Workflows"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; --var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; --function SetSection({ title, children }) { -- return (0, import_react41.createElement)("section", { className: "bt-set-section" }, (0, import_react41.createElement)("h3", null, title), (0, import_react41.createElement)("div", { className: "bt-set-card" }, children)); -+function chainOf(item) { -+ if (typeof item?.chain === "string" && item.chain) return item.chain; -+ if (!Array.isArray(item?.layers) || !Array.isArray(item?.nodes)) return ""; -+ const name = (id) => item.nodes.find((node) => node.id === id)?.name ?? id; -+ return item.layers.map((layer) => layer.map(name).join(" ‖ ")).join(" → "); - } --function SetRow({ label, hint, children }) { -- return (0, import_react41.createElement)( -- "div", -- { className: "bt-set-row" }, -- (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, label), hint ? (0, import_react41.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- children ? (0, import_react41.createElement)("div", { className: "bt-set-control" }, children) : null -- ); -+function openedGraph(pack, opened) { -+ if (!pack || !opened) return null; -+ if (opened.source === "own") return (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; -+ return (pack.templates ?? []).find((item) => item.id === opened.id) ?? (pack.workflows ?? []).find((item) => item.id === opened.id) ?? null; - } --var NOTIFY_KINDS = [ -- ["approval", "Waiting for approval", "Once more if it still waits after 15 min."], -- ["finished", "Finished"], -- ["failed", "Failed"] --]; --function GeneralPage({ roster, actions, run }) { -- const prefs = roster.prefs ?? {}; -- const themes = allThemes(); -- const current = themes[prefs.theme] ? prefs.theme : "deepseek"; -- const [custom, setCustom] = (0, import_react41.useState)(prefs.accent ?? "#4d6bfe"); -- const timer = (0, import_react41.useRef)(0); -- (0, import_react41.useEffect)(() => () => clearTimeout(timer.current), []); -- const pickCustom = (value) => { -- setCustom(value); -- clearTimeout(timer.current); -- timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350); -- }; -- return [ -- (0, import_react41.createElement)( -- SetSection, -- { key: "look", title: t("Appearance") }, -- (0, import_react41.createElement)( -- SetRow, -- { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, -- (0, import_react41.createElement)( -- "select", -- { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, -- Object.entries(themes).map(([id, theme]) => (0, import_react41.createElement)("option", { key: id, value: id }, t(theme.label))) -- ) -- ), -- (0, import_react41.createElement)( -- SetRow, -- { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, -- (0, import_react41.createElement)( -- "div", -- { className: "bt-swatches" }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), -- ACCENT_INKS.map((ink) => (0, import_react41.createElement)("button", { -- key: ink, -- type: "button", -- className: "bt-swatch", -- title: t(capitalize2(ink)), -- "aria-label": t(capitalize2(ink)), -- "aria-pressed": prefs.accent === INKS[ink][0], -- style: { background: INKS[ink][0] }, -- onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) -- })), -- (0, import_react41.createElement)( -- "label", -- { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, -- (0, import_react41.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) -- ) -- ) -- ) -- ), -- // One switch for every system notification (#353), then one per kind (#469). -- (0, import_react41.createElement)( -- SetSection, -- { key: "notify", title: t("Notifications") }, -- (0, import_react41.createElement)(SwitchRow, { -- label: t("In other chats"), -- hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), -- on: prefs.notifications !== false, -- onChange: (on) => run(actions.setPrefs({ notifications: on })) -- }), -- NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react41.createElement)(SwitchRow, { -- key: kind, -- label: t(label), -- hint: hint ? t(hint) : void 0, -- disabled: prefs.notifications === false, -- on: prefs.notify?.[kind] !== false, -- onChange: (on) => run(actions.setPrefs({ notify: { [kind]: on } })) -- })) -- ) -- ]; -+function statusLabel2(status) { -+ if (status === "running") return t("Running"); -+ if (status === "done") return t("Done"); -+ if (status === "failed") return t("Failed"); -+ if (status === "idle") return t("Idle"); -+ if (status === "skipped") return t("Skipped"); -+ return t("Queued"); - } --function MainBotsSection({ roster, actions, run }) { -- const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean); -- const [first, ...others] = mains; -- const own = roster.bots.filter((bot) => !bot.remote); -- const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); -- return (0, import_react41.createElement)( -- SetSection, -- { title: t("Main Bot") }, -- (0, import_react41.createElement)( -- SetRow, -- { -- label: t("Main Bot"), -- hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 -- }, -- (0, import_react41.createElement)("select", { -- className: "bt-select", -- "aria-label": t("Main Bot"), -- value: first?.id ?? "", -- onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) -- }, first ? null : (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -- ), -- others.map((bot) => (0, import_react41.createElement)( -- SetRow, -- { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) -- )), -- candidates.length > 0 ? (0, import_react41.createElement)( -- SetRow, -- { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, -- (0, import_react41.createElement)("select", { -- className: "bt-select", -- "aria-label": t("Add a Main Bot"), -- value: "", -- onChange: (event) => { -- if (event.target.value) void run(actions.setMain(event.target.value, true)); -- } -- }, (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -- ) : null -- ); -+function hitNode(event) { -+ const direct = event.target?.dataset?.node ? event.target : null; -+ const nested = event.target?.closest?.("[data-node]"); -+ return (nested ?? direct)?.dataset?.node || ""; - } --var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]]; --function MotionSection({ roster, actions, run }) { -- const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; -- return (0, import_react41.createElement)( -- SetSection, -- { title: t("Animation") }, -- (0, import_react41.createElement)( -- SetRow, -- { label: t("Bot animation") }, -- (0, import_react41.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react41.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": motion === id, -- onClick: () => { -- if (motion !== id) run(actions.setPrefs({ motion: id })); -- } -- }, t(label)))) -- ) -- ); -+var WIRE_SPAN = 480; -+var NODE_WIRE_OUT = 56; -+var CARD_HALF = 84; -+var CARD_H = 72; -+var FRAME_SIDE = 108; -+var FRAME_LABEL = 18; -+var FRAME_FOOT = 10; -+var MIN_FIT = 0.5; -+function wirePath(from, to) { -+ const x1 = from.x + WIRE_SPAN; -+ const y1 = from.y + NODE_WIRE_OUT; -+ const x2 = to.x + WIRE_SPAN; -+ const y2 = to.y; -+ const mid = (y1 + y2) / 2; -+ return `M ${x1} ${y1} C ${x1} ${mid}, ${x2} ${mid}, ${x2} ${y2}`; - } --function BotsPage({ roster, actions, run }) { -- const [creating, setCreating] = (0, import_react41.useState)(false); -- return [ -- (0, import_react41.createElement)(MainBotsSection, { key: "main", roster, actions, run }), -- (0, import_react41.createElement)(MotionSection, { key: "motion", roster, actions, run }), -- creating ? (0, import_react41.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react41.createElement)( -- "div", -- { key: "new", className: "bt-actions" }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("New Bot")) -- ), -- (0, import_react41.createElement)( -- SetSection, -- { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, -- roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react41.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, -- (0, import_react41.createElement)( -- "span", -- { className: "bt-set-bot" }, -- (0, import_react41.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), -- (0, import_react41.createElement)("span", { className: "bt-row-title" }, bot.name), -- bot.role ? (0, import_react41.createElement)("span", { className: "bt-tag" }, bot.role) : null, -- isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, -- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -- ), -- (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon)) -- )) -- ) -- ]; -+function fitScale(nodes, width) { -+ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -+ if (!(width > 0) || reach === 0) return 1; -+ return Math.min(1, Math.max(MIN_FIT, width / 2 / reach)); - } --function GroupsPage({ roster, actions, run }) { -- const [creating, setCreating] = (0, import_react41.useState)(false); -- return [ -- creating ? (0, import_react41.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react41.createElement)( -- "div", -- { key: "new", className: "bt-actions" }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("Create group chat")) -- ), -- (0, import_react41.createElement)( -- SetSection, -- { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, -- roster.rooms.length === 0 ? (0, import_react41.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { -- const admin = roster.byId[room2.admin]; -- return (0, import_react41.createElement)( -- "button", -- { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, -- (0, import_react41.createElement)( -- "span", -- { className: "bt-set-bot" }, -- (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react41.createElement)("span", { className: "bt-row-title" }, room2.name), -- (0, import_react41.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), -- admin ? (0, import_react41.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react41.createElement)(AdminStar), admin.name) : null, -- room2.hidden ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -- ), -- (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon)) -- ); -- }) -- ) -- ]; -+function fitLayout(nodes, width) { -+ const reach = Math.max(0, ...(nodes ?? []).map((node) => Math.abs(node.x ?? 0) + FRAME_SIDE)); -+ const scale = fitScale(nodes, width); -+ const spare = width > 0 && reach > 0 ? reach * scale - width / 2 : 0; -+ const pad2 = spare > 1e-6 ? Math.ceil(spare) : 0; -+ const centre = width > 0 ? pad2 + width / 2 : 0; -+ return { -+ reach, -+ scale, -+ pad: pad2, -+ left: centre - reach * scale, -+ right: centre + reach * scale, -+ scrollWidth: Math.max(width + pad2, centre + reach * scale) -+ }; - } --function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { -- const [error, setError] = (0, import_react41.useState)(""); -- const close = (0, import_react41.useCallback)(() => actions.closeSettings(), []); -- useEscape(close); -- const run = (promise) => Promise.resolve(promise).then(() => { -- setError(""); -- return true; -- }).catch((failure) => { -- setError(failure?.message ?? String(failure)); -- return false; -- }); -- const bot = page === "bots" && id ? roster.byId[id] : void 0; -- const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; -- const extra = useSettingsPages().map((entry) => ({ entry, key: `plugin:${entry.id}` })); -- const pages = SETTINGS_PAGES.flatMap((item) => item[0] === "connectors" ? [item, ...extra.map(({ entry, key }) => [key, entry.label ?? entry.id])] : [item]); -- const shown = pages.some(([key]) => key === page) ? page : "general"; -- const pluginPage = extra.find(({ key }) => key === shown)?.entry; -- const usageFocus = shown === "usage" && id ? id : null; -- const label = t(pages.find(([key]) => key === shown)[1]); -- const back = bot ? t("Back to Bots") : room2 ? t("Back to group chats") : usageFocus ? t("All usage") : null; -- const nav = useGlide(shown, { axis: "y" }); -- const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; -- const depth = bot || room2 ? 1 : 0; -- const last = (0, import_react41.useRef)({ key: pageKey, depth, move: "up" }); -- if (last.current.key !== pageKey) { -- last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; -- } -- const [usageOpened, setUsageOpened] = (0, import_react41.useState)(shown === "usage"); -- if (shown === "usage" && !usageOpened) setUsageOpened(true); -- const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -- return (0, import_react41.createElement)( -- "div", -- { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, -- (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -- (0, import_react41.createElement)( -- "div", -- { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, -- (0, import_react41.createElement)( -- "nav", -- { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, -- (0, import_react41.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), -- pages.map(([key, text2]) => (0, import_react41.createElement)("button", { -- key, -- type: "button", -- className: "bt-settings-item", -- "aria-current": shown === key ? "page" : void 0, -- onClick: () => actions.openSettings(key) -- }, (0, import_react41.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))), -- // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot -- // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). -- (0, import_react41.createElement)("button", { -- type: "button", -- className: "bt-settings-item bt-settings-shell", -- title: t("Opens DeepSeek Harness's own settings (models, providers, permissions) in a separate window"), -- "aria-label": t("Harness settings, opens DeepSeek Harness's own settings window"), -- onClick: () => { -- close(); -- openHarnessSettings(); -- } -- }, (0, import_react41.createElement)(SlidersIcon), t("Harness settings"), (0, import_react41.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) -- ), -- (0, import_react41.createElement)( -- "div", -- { className: "bt-settings-main" }, -- (0, import_react41.createElement)( -- "div", -- { className: "bt-settings-head" }, -- back ? (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react41.createElement)(ChevronLeftIcon)) : null, -- (0, import_react41.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) -- ), -- shown === "usage" ? null : (0, import_react41.createElement)( -- "div", -- { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, -- errorLine, -- shown === "general" ? (0, import_react41.createElement)(GeneralPage, { roster, actions, run }) : null, -- bot ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, -- shown === "bots" && !bot ? (0, import_react41.createElement)(BotsPage, { roster, actions, run }) : null, -- room2 ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, -- shown === "groups" && !room2 ? (0, import_react41.createElement)(GroupsPage, { roster, actions, run }) : null, -- shown === "projects" ? (0, import_react41.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null, -- shown === "connectors" ? (0, import_react41.createElement)(ConnectorsPage, { roster, actions }) : null, -- shown === "workflows" ? (0, import_react41.createElement)(WorkflowsPage, { call: call2, roster }) : null, -- pluginPage ? (0, import_react41.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, -- shown === "capabilities" ? (0, import_react41.createElement)(CapabilitiesPage, { roster, actions, run }) : null, -- shown === "schedules" ? (0, import_react41.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, -- shown === "secrets" ? (0, import_react41.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, -- shown === "feedback" ? (0, import_react41.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, -- shown === "about" ? (0, import_react41.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null -- ), -- usageOpened ? (0, import_react41.createElement)( -- "div", -- { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, -- shown === "usage" ? errorLine : null, -- (0, import_react41.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) -- ) : null -- ), -- (0, import_react41.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) -- ) -- ); -+function measureCanvasWidth(el) { -+ const width = el?.clientWidth; -+ return Number.isFinite(width) && width > 0 ? width : 0; - } --var openProjectBoard = (id) => { -- if (typeof window !== "undefined") window.location.hash = `#project=${id}`; --}; --var projectMembers = (roster) => [ -- ...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })), -- ...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null })) --]; --function ProjectsList({ projects, roster, call: call4, reload }) { -- const members = projectMembers(roster); -- const bind = async (choice, project) => { -- const [kind, id] = String(choice ?? "").split(":"); -- if (!kind || !id) return; -- await call4("project-bind", { kind, id, projectId: project.id }); -- await reload?.(); -- }; -- const unbind = async (member) => { -- await call4("project-bind", { kind: member.kind, id: member.id, projectId: null }); -- await reload?.(); -+function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) { -+ const gesture = (0, import_react37.useRef)(null); -+ const canvasRef = (0, import_react37.useRef)(null); -+ const [measuredWidth, setCanvasWidth] = (0, import_react37.useState)(0); -+ const nodes = graph.nodes ?? []; -+ const edges = graph.edges ?? []; -+ const status = graph.run?.nodeStatus ?? null; -+ const locals = (roster?.bots ?? []).filter((bot) => bot && bot.remote !== true && !nodes.some((node) => node.botId === bot.id)); -+ const height = Math.max(420, ...nodes.map((node) => (node.y ?? 0) + 150), 0); -+ const canvasWidth = measuredWidth || knownWidth; -+ const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth); -+ const drawnHeight = Math.round(height * scale); -+ (0, import_react37.useEffect)(() => { -+ const el = canvasRef.current; -+ const measure = () => { -+ const width = measureCanvasWidth(el); -+ if (width > 0 && Math.abs(width - canvasWidth) > 0.5) setCanvasWidth(width); -+ }; -+ measure(); -+ if (typeof ResizeObserver !== "function" || !el) return void 0; -+ const observer = new ResizeObserver(measure); -+ observer.observe(el); -+ return () => observer.disconnect(); -+ }, [canvasWidth]); -+ const end = (event) => { -+ const current = gesture.current; -+ gesture.current = null; -+ if (!current || !editing) return; -+ if (current.kind === "move") { -+ const x = Math.round(current.x + (event.clientX - current.ox) / scale); -+ const y = Math.round(current.y + (event.clientY - current.oy) / scale); -+ onCommit?.({ nodes: moveNode(nodes, current.id, x, y), edges }); -+ } else if (current.kind === "wire") { -+ const next = connectNodes(edges, current.from, hitNode(event)); -+ if (next !== edges) onCommit?.({ nodes, edges: next }); -+ } else if (current.kind === "add" && event.target?.dataset?.plane) { -+ const x = Number.isFinite(event.offsetX) ? Math.round(event.offsetX / scale) : 24; -+ const y = Number.isFinite(event.offsetY) ? Math.round(event.offsetY / scale) : 24; -+ onCommit?.({ nodes: dropBot(nodes, current.bot, x, y), edges }); -+ } - }; -- const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0); -- return [ -- (0, import_react41.createElement)( -- SetSection, -- { key: "projects", title: t("Projects · {count}", { count: projects.length }) }, -- projects.length === 0 ? (0, import_react41.createElement)("div", { className: "bt-set-row" }, (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react41.createElement)( -- SetRow, -- { -- key: project.id, -- label: project.name, -- hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir -- }, -- (0, import_react41.createElement)( -+ return (0, import_react37.createElement)( -+ "div", -+ { 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", { -+ key: bot.id, -+ type: "button", -+ "data-bot": bot.id, -+ onPointerDown: (event) => { -+ event.preventDefault?.(); -+ gesture.current = { kind: "add", bot }; -+ } -+ }, bot.name)) -+ ) : null, -+ // A node's system prompt is the run Bot's prompt (plugins/workflow -+ // nodeConfig). The sidebar panel has this field, so the Settings editor -+ // writes the same graph instead of only drawing it (#1393). -+ editing ? (0, import_react37.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react37.createElement)( -+ "label", -+ { key: node.id, className: "bt-wf-prompt" }, -+ (0, import_react37.createElement)("span", null, `${node.name} · ${t("System prompt")}`), -+ (0, import_react37.createElement)("textarea", { -+ "data-prompt": node.id, -+ rows: 3, -+ defaultValue: node.prompt || "", -+ onPointerDown: (event) => event.stopPropagation?.(), -+ onBlur: (event) => { -+ const prompt = event.target?.value ?? ""; -+ if (prompt === (node.prompt || "")) return; -+ onCommit?.({ nodes: nodes.map((item) => item.id === node.id ? { ...item, prompt } : item), edges }); -+ } -+ }) -+ ))) : null, -+ (0, import_react37.createElement)( -+ "div", -+ { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, -+ (0, import_react37.createElement)( -+ "div", -+ { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, -+ (0, import_react37.createElement)( - "div", -- { className: "bt-project-row-controls" }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")), -- (0, import_react41.createElement)( -- "select", -+ { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, -+ (0, import_react37.createElement)( -+ "svg", - { -- className: "bt-select", -- "aria-label": t("Bind to {name}", { name: project.name }), -- value: "", -- onChange: (event) => { -- void bind(event.target.value, project); -- } -+ className: "bt-wf-wires", -+ "aria-hidden": true, -+ width: WIRE_SPAN * 2, -+ height, -+ viewBox: `0 0 ${WIRE_SPAN * 2} ${height}` - }, -- (0, import_react41.createElement)("option", { value: "" }, t("Bind a Bot or group")), -- members.map((member) => (0, import_react41.createElement)( -- "option", -- { key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` }, -- `${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}` -- )) -- ) -+ (0, import_react37.createElement)( -+ "defs", -+ null, -+ (0, import_react37.createElement)("marker", { -+ id: "bt-wf-arrow", -+ viewBox: "0 0 10 10", -+ refX: "8", -+ refY: "5", -+ markerWidth: "6", -+ markerHeight: "6", -+ orient: "auto" -+ }, (0, import_react37.createElement)("path", { d: "M0 0 10 5 0 10z" })) -+ ), -+ edges.map((edge) => { -+ const from = nodes.find((node) => node.id === edge.from); -+ const to = nodes.find((node) => node.id === edge.to); -+ if (!from || !to) return null; -+ return (0, import_react37.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" }); -+ }) -+ ), -+ (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => { -+ const members = layer.map((id) => nodes.find((node) => node.id === id)).filter(Boolean); -+ if (members.length < 2) return null; -+ const left = Math.min(...members.map((node) => node.x ?? 0)) - FRAME_SIDE; -+ const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE; -+ const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL; -+ const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT; -+ return (0, import_react37.createElement)("div", { -+ key: layer.join("+"), -+ className: "bt-wf-par", -+ style: { left, top, width: right - left, height: bottom - top } -+ }, (0, import_react37.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join"))); -+ }), -+ nodes.map((node) => { -+ const state = status ? status[node.id] ?? "queued" : "idle"; -+ return (0, import_react37.createElement)( -+ "div", -+ { -+ key: node.id, -+ "data-node": node.id, -+ className: `bt-wf-node ${state}${editing ? " editing" : ""}`, -+ style: { left: (node.x ?? 0) - CARD_HALF, top: node.y ?? 0 }, -+ onPointerDown: (event) => { -+ if (!editing || event.target?.dataset?.port || event.target?.dataset?.remove) return; -+ gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 }; -+ } -+ }, -+ editing ? (0, import_react37.createElement)("button", { -+ type: "button", -+ className: "bt-wf-port", -+ "data-port": "out", -+ "aria-label": node.name, -+ onPointerDown: (event) => { -+ event.preventDefault?.(); -+ event.stopPropagation?.(); -+ gesture.current = { kind: "wire", from: node.id }; -+ } -+ }) : null, -+ editing ? (0, import_react37.createElement)("button", { -+ type: "button", -+ className: "bt-wf-remove", -+ "data-remove": node.id, -+ "aria-label": t("Remove"), -+ onClick: (event) => { -+ event.stopPropagation?.(); -+ const next = removeNode(nodes, edges, node.id); -+ onCommit?.(next); -+ } -+ }, "×") : null, -+ (0, import_react37.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)), -+ (0, import_react37.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)), -+ (0, import_react37.createElement)( -+ "span", -+ { className: "bt-wf-copy" }, -+ (0, import_react37.createElement)("span", { className: "bt-wf-name" }, node.name), -+ (0, import_react37.createElement)("span", { className: "bt-wf-role" }, node.role ?? "") -+ ) -+ ); -+ }) - ) -- )) -- ), -- bound2.length === 0 ? null : (0, import_react41.createElement)( -- SetSection, -- { key: "bindings", title: t("Bindings") }, -- bound2.map((member) => (0, import_react41.createElement)( -- SetRow, -- { -- key: `${member.kind}:${member.id}`, -- label: member.name, -- hint: member.kind === "group" ? t("Group") : t("Bot") -- }, -- (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -- void unbind(member); -- } }, t("Unbind")) -- )) -+ ) - ) -- ]; -+ ); - } --function ProjectsPage({ roster, actions, run, call: call4 }) { -- const [state, setState] = (0, import_react41.useState)({ projects: [], error: null }); -- const [name, setName] = (0, import_react41.useState)(""); -- const [workDir, setWorkDir] = (0, import_react41.useState)(null); -- const reload = (0, import_react41.useCallback)(async () => { -- const listing = await call4("projects"); -- setState({ projects: listing?.projects ?? [], error: null }); -- }, [call4]); -- (0, import_react41.useEffect)(() => { -- void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) })); -- }, [reload]); -- const create = async () => { -- const trimmed3 = name.trim(); -- if (trimmed3 === "") return; -- const body = { name: trimmed3 }; -- if (workDir !== null && workDir !== "") body.workDir = workDir; -- try { -- await call4("project-create", body); -- setName(""); -- setWorkDir(null); -- await reload(); -- } catch (error) { -- setState({ projects: state.projects, error: error?.message ?? String(error) }); -- } -- }; -- return [ -- state.error === null ? null : (0, import_react41.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error), -- (0, import_react41.createElement)( -- SetSection, -- { key: "new", title: t("New Project") }, -- (0, import_react41.createElement)( -- SetRow, -- { label: t("Name") }, -- (0, import_react41.createElement)("input", { -- className: "bt-input", -- "aria-label": t("Project name"), -- value: name, -- placeholder: t("e.g. Website"), -- onChange: (event) => setName(event.target.value) -- }) -- ), -- (0, import_react41.createElement)( -- SetRow, -- { label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") }, -- (0, import_react41.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir }) -- ), -- (0, import_react41.createElement)( -- "div", -- { className: "bt-actions" }, -- (0, import_react41.createElement)("button", { -- type: "button", -- className: "bt-primary bt-with-icon", -- disabled: name.trim() === "", -- onClick: () => { -- void create(); -- } -- }, (0, import_react41.createElement)(PlusIcon), t("New Project")) -- ) -+function Row2({ item, source, onOpen, onRename, onDelete }) { -+ return (0, import_react37.createElement)( -+ "button", -+ { -+ type: "button", -+ className: "bt-set-row", -+ "data-workflow": item.id, -+ "data-source": source, -+ onClick: () => onOpen?.(item) -+ }, -+ (0, import_react37.createElement)( -+ "span", -+ { 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 || "") - ), -- // The list is rendered here rather than as a child element: it holds no state of its own, -- // and the page keeps one reload path for both halves. -- ...ProjectsList({ projects: state.projects, roster, call: call4, reload }) -- ]; --} -- --// src/client/context-menu.js --var import_react42 = require("react"); --function ContextMenu({ menu, roster, actions, leaving = false }) { -- useEscape(() => actions.closeMenu()); -- (0, import_react42.useEffect)(() => { -- const onDown = (event) => { -- if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); -- }; -- window.addEventListener("mousedown", onDown); -- return () => window.removeEventListener("mousedown", onDown); -- }, []); -- const structure = useSidebarStructure(); -- const ref = (0, import_react42.useRef)(null); -- const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y }); -- (0, import_react42.useLayoutEffect)(() => { -- const box = ref.current?.getBoundingClientRect(); -- if (!box) return; -- setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -- }, [menu.x, menu.y]); -- if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react42.createElement)(AgentMenu, { menu, roster, actions, leaving }); -- const bot = roster.byId[menu.id]; -- const room2 = roster.roomsById[menu.id]; -- const entry = bot ?? room2; -- if (entry === void 0) return null; -- const isMainBot = isMainOf(roster, menu.id); -- const isTile = roster.mainBotId === menu.id; -- const mainCount = (roster.mainBotIds ?? []).length; -- const groups = bot?.remote ? [ -- [ -- [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -- ], -- [ -- [t("Bot settings"), GearIcon, () => actions.openSettings("bots", bot.id)] -- ], -- pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]), -- [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(currentPart(bot))]] -- ] : [ -- [ -- isTile ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })], -- [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -- ], -- [ -- // One way to edit a Bot (#385): its name is edited in Edit Bot too. -- bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, -- room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, -- room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -- bot ? [t("Duplicate"), DuplicateIcon, async () => { -- const copy2 = await actions.duplicateBot(bot.id); -- actions.openSession(copy2.id); -- }] : null, -- bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, -- bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null -- ], -- bot ? pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]) : [], -- [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(bot ? currentPart(bot) : menu.id)]], -- [ -- isMainBot ? null : [t("Hide from sidebar"), HideIcon, () => actions.setFlags(menu.id, { hidden: true })], -- isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true] -- ] -- ]; -- const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); -- return (0, import_react42.createElement)( -- "div", -- { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, -- shown.map((group, index) => (0, import_react42.createElement)( -- import_react42.Fragment, -- { key: index }, -- index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", { -- key: label, -+ source === "own" ? (0, import_react37.createElement)( -+ "span", -+ { className: "bt-set-actions" }, -+ (0, import_react37.createElement)("button", { - type: "button", -- role: "menuitem", -- className: danger ? "bt-danger" : void 0, -- onClick: () => { -- actions.closeMenu(); -- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); -+ className: "bt-set-action", -+ "aria-label": t("Rename"), -+ title: t("Rename"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onRename?.(item); - } -- }, (0, import_react42.createElement)(Icon), label)) -- )) -- ); --} --function AgentMenu({ menu, roster, actions, leaving }) { -- const ref = (0, import_react42.useRef)(null); -- const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y }); -- (0, import_react42.useLayoutEffect)(() => { -- const box = ref.current?.getBoundingClientRect(); -- if (!box) return; -- setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -- }, [menu.x, menu.y]); -- const agent = roster.agentsById[menu.kind === "agent" ? menu.id : menu.agentId]; -- if (agent === void 0) return null; -- const groups = menu.kind === "agent" ? [ -- [ -- [agent.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(agent.id, { pinned: !agent.pinned })] -- ], -- [ -- [t("Remove DSH Agent"), TrashIcon, () => actions.removeAgent(agent.id), true] -- ] -- ] : [ -- [ -- [t("Rename"), PencilIcon, () => actions.beginAgentRename(menu.id)] -- ], -- [ -- [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] -- ] -- ]; -- return (0, import_react42.createElement)( -- "div", -- { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, -- groups.map((group, index) => (0, import_react42.createElement)( -- import_react42.Fragment, -- { key: index }, -- index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", { -- key: label, -+ }, "✎"), -+ (0, import_react37.createElement)("button", { - type: "button", -- role: "menuitem", -- className: danger ? "bt-danger" : void 0, -- onClick: () => { -- actions.closeMenu(); -- void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); -+ className: "bt-set-action", -+ "aria-label": t("Delete"), -+ title: t("Delete"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onDelete?.(item); - } -- }, (0, import_react42.createElement)(Icon), label)) -- )) -+ }, "×") -+ ) : null, -+ (0, import_react37.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›") - ); - } -- --// src/client/pane-width.js --var DEFAULT_WIDTH = 520; --var DETAILS_WIDTH = 380; --var DETAILS_MAX = 520; --var MIN_WIDTH = 360; --var MIN_CHAT = 420; --var MIN_DETAILS = 240; --function panelWidthRange(view, viewport) { -- const room2 = Math.max(0, viewport - MIN_CHAT); -- if (view === "browser") return [MIN_WIDTH, Math.max(MIN_WIDTH, room2)]; -- const low = Math.max(MIN_DETAILS, Math.min(DETAILS_WIDTH, room2)); -- return [low, Math.max(low, Math.min(DETAILS_MAX, room2))]; --} -- --// src/client/storage.js --var STORAGE = { -- unread: "ds-bot:unread", -- reactions: "ds-bot:reactions", -- seen: "ds-bot:seen", -- friendLines: "ds-bot:friend-lines", -- prefs: "ds-bot:prefs", -- surface: "ds-bot:surface", -- agentSessions: "ds-bot:agent-sessions", -- browserWidth: "ds-bot:browser-width", -- browsers: "ds-bot.browser.v2" --}; --var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"]; --var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"]; --var LEGACY_THEMES = { grok: "mono" }; --function legacyKeys(key) { -- if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS; -- const name = key.slice(key.indexOf(":") + 1); -- return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`); --} --var migrateBrowsers = (storage) => { -- if (storage.getItem(STORAGE.browsers) !== null) { -- for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key); -- return; -- } -- for (const key of LEGACY_BROWSER_KEYS) { -- try { -- const value = storage.getItem(key); -- if (value === null) continue; -- const previous = JSON.parse(value) ?? {}; -- const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => { -- const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" }; -- return [botId, { tabs: [tab], active: tab.id }]; -- })); -- storage.setItem(STORAGE.browsers, JSON.stringify(tabs)); -- for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier); -- return; -- } catch { -- } -- } --}; --function migrateStorage(storage = globalThis.localStorage) { -- for (const key of Object.values(STORAGE)) { -- if (key === STORAGE.browsers) continue; -- for (const legacy of legacyKeys(key)) { -- try { -- const value = storage.getItem(legacy); -- if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); -- storage.removeItem(legacy); -- } catch { -- } -- } -- } -- try { -- migrateBrowsers(storage); -- } catch { -- } -- try { -- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -- } -- } catch { -- } -+function alertCopy(error) { -+ if (error === WORKFLOW_PLUGIN_HINT) return t("The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."); -+ return error || t("Could not read workflows"); - } -- --// src/client/details-drawer.js --var import_react43 = require("react"); --var TAB_LABELS = { details: "Details", browser: "Browser" }; --var hostOf = (url) => { -- try { -- return new URL(url).hostname.replace(/^www\./, ""); -- } catch { -- return ""; -- } --}; --var shortUrl = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, ""); --var EMPTY_TABS = { ids: [], active: null }; --var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; --var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); --function PermissionRow({ bot, roster, actions, run }) { -- const [pending, setPending] = (0, import_react43.useState)(null); -- const catalog = roster.permissions; -- const saved = bot.permission ?? catalog?.defaultPreset; -- (0, import_react43.useEffect)(() => { -- if (pending === saved) setPending(null); -- }, [pending, saved]); -- if (!catalog?.options?.length) return null; -- const current = pending ?? saved; -- const pick2 = (value) => { -- if (value === current && bot.permission !== void 0) return; -- setPending(value); -- void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); -- }; -- return (0, import_react43.createElement)( -+function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, canvasWidth = 0, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) { -+ const graph = phase === "ready" ? openedGraph(pack, opened) : null; -+ const templates = pack?.templates ?? []; -+ const templateIds = new Set(templates.map((item) => item.id)); -+ const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); -+ const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; -+ return (0, import_react37.createElement)( - "div", -- { className: "bt-drawer-permission" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Permissions")), -- (0, import_react43.createElement)( -- Segmented, -- { label: t("Permissions"), value: current }, -- catalog.options.map((option) => (0, import_react43.createElement)("button", { -- key: option.value, -- type: "button", -- "aria-pressed": option.value === current, -- title: option.description, -- onClick: () => pick2(option.value), -- className: DANGER.has(option.value) ? "bt-perm-danger" : void 0 -- }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name)) -- ) -+ { className: "bt-wf-page" }, -+ (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")), -+ error ? (0, import_react37.createElement)( -+ "div", -+ { className: "bt-error", role: "alert" }, -+ alertCopy(error), -+ phase === "error" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null -+ ) : null, -+ phase === "loading" ? (0, import_react37.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null, -+ phase === "empty" ? (0, import_react37.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null, -+ phase === "ready" && !graph ? (0, import_react37.createElement)( -+ "div", -+ { className: "bt-wf-lists" }, -+ (0, import_react37.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (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", -+ { 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); -+ const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item; -+ return (0, import_react37.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); -+ }) -+ ) -+ ), -+ (0, import_react37.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react37.createElement)("h3", null, t("Yours")), -+ (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), -+ (0, import_react37.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ 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", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) -+ ) -+ ) -+ ) : null, -+ graph ? (0, import_react37.createElement)( -+ "div", -+ { className: "bt-wf-editor" }, -+ (0, import_react37.createElement)( -+ "div", -+ { className: "bt-wf-actions" }, -+ (0, import_react37.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), -+ showUse ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, -+ opened?.source === "own" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null -+ ), -+ (0, import_react37.createElement)("h3", { className: "bt-wf-title" }, graph.name), -+ graph.description ? (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, -+ (graph.nodes ?? []).length === 0 ? (0, import_react37.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react37.createElement)(GraphCanvas, { -+ graph, -+ roster, -+ onCommit, -+ width: canvasWidth, -+ editing: opened?.source === "own" && editing === true -+ }) -+ ) : null - ); - } --function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { -- const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); -- const pages = useBrowserPages((map) => map); -- (0, import_react43.useEffect)(() => { -- actions.browserEnsure?.(bot.id); -- }, [bot.id]); -- if (roster.browser !== true || !actions.browserShow) { -- return (0, import_react43.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); -- } -- if (state.ids.length === 0) { -- return (0, import_react43.createElement)( -- "div", -- { className: "bt-browser-empty-mark" }, -- (0, import_react43.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), -- (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -- (0, import_react43.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), -- (0, import_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) -- ); -- } -- return (0, import_react43.createElement)( -+function RenameModal({ workflow, onClose, onConfirm }) { -+ const [name, setName] = (0, import_react37.useState)(workflow?.name ?? ""); -+ return (0, import_react37.createElement)( - "div", -- { className: "bt-bcards" }, -- state.ids.map((tabId, index) => { -- const page = pages[tabId] ?? {}; -- const label = page.title || shortUrl(page.url) || t("New tab"); -- return (0, import_react43.createElement)( -+ { className: "bt-modal-mask", onClick: (event) => { -+ if (event.target === event.currentTarget) onClose?.(); -+ } }, -+ (0, import_react37.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)( -+ "label", -+ { className: "bt-field" }, -+ t("Name"), -+ (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) -+ ), -+ (0, import_react37.createElement)( - "div", -- { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, -- (0, import_react43.createElement)( -- "button", -- { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, -- page.excerpt ? (0, import_react43.createElement)( -- "span", -- { className: "bt-bcard-preview" }, -- (0, import_react43.createElement)("span", { className: "bt-bcard-ptitle" }, label), -- (0, import_react43.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) -- ) : (0, import_react43.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react43.createElement)(GlobeIcon), (0, import_react43.createElement)("span", null, label)) -- ), -- (0, import_react43.createElement)("button", { -- type: "button", -- className: "bt-bcard-x", -- "aria-label": t("Close tab"), -- title: t("Close tab"), -- onClick: () => { -- void actions.browserCloseTab(bot.id, tabId); -- } -- }, "×"), -- (0, import_react43.createElement)( -- "div", -- { className: "bt-bcard-row" }, -- (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), -- (0, import_react43.createElement)("span", { className: "bt-bcard-title" }, label), -- page.url ? (0, import_react43.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null -- ) -- ); -- }), -- (0, import_react43.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react43.createElement)(PlusIcon), t("New tab")) -+ { 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")) -+ ) -+ ) - ); - } --function About({ text: text2, title = "About" }) { -- const [open, setOpen] = (0, import_react43.useState)(false); -- const long = text2.split("\n").length > 5 || text2.length > 240; -- const body = (0, import_react43.useRef)(null); -- useMorph(body, open); -- return (0, import_react43.createElement)( -+function DeleteModal({ workflow, onClose, onConfirm }) { -+ return (0, import_react37.createElement)( - "div", -- null, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react43.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), -- long ? (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null -+ { className: "bt-modal-mask", onClick: (event) => { -+ if (event.target === event.currentTarget) onClose?.(); -+ } }, -+ (0, import_react37.createElement)( -+ "div", -+ { 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-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")) -+ ) -+ ) - ); - } --function SharedRoomApprovals({ room: room2 }) { -- const [items, setItems] = (0, import_react43.useState)(room2.pendingApprovals ?? []); -- const [busy, setBusy] = (0, import_react43.useState)(""); -- const face = room2.origin === "face-to-face"; -- (0, import_react43.useEffect)(() => { -- const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; -- if (haveItems) setItems(room2.pendingApprovals); -- if (room2.viewerRole !== "invited-owner" && !face) return void 0; -- let alive = true; -- call("social/status", {}).then((value) => { -- if (!alive) return; -- if (room2.viewerRole === "invited-owner" && !haveItems) { -- setItems(Array.isArray(value?.approvals) ? value.approvals : []); -- } -- }, () => { -+function WorkflowsPage({ call: call4, roster }) { -+ const [phase, setPhase] = (0, import_react37.useState)("loading"); -+ const [pack, setPack] = (0, import_react37.useState)(null); -+ const [error, setError] = (0, import_react37.useState)(""); -+ const [opened, setOpened] = (0, import_react37.useState)(null); -+ const [editing, setEditing] = (0, import_react37.useState)(false); -+ const [renaming, setRenaming] = (0, import_react37.useState)(null); -+ const [deleting, setDeleting] = (0, import_react37.useState)(null); -+ const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows")); -+ const apply2 = (settled) => { -+ setPhase(settled.phase); -+ setPack(settled.pack); -+ setError(settled.error); -+ }; -+ (0, import_react37.useEffect)(() => { -+ let gone = false; -+ readWorkflows(invoke).then((settled) => { -+ if (!gone) apply2(settled); - }); - return () => { -- alive = false; -+ gone = true; - }; -- }, [room2.id, room2.viewerRole, room2.pendingCount, face]); -- if (room2.viewerRole !== "invited-owner" && !face || !items.length) return null; -- const decide = async (item, decision) => { -- setBusy(item.id); -- try { -- await call("social/decide-approval", { approvalId: item.id, decision }); -- setItems((list) => list.filter((entry) => entry.id !== item.id)); -- } finally { -- setBusy(""); -+ }, []); -+ const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); -+ (0, import_react37.useEffect)(() => { -+ if (!live || editing) return void 0; -+ let gone = false; -+ const timer = setInterval(() => { -+ readWorkflows(invoke).then((settled) => { -+ if (!gone) apply2(settled); -+ }); -+ }, POLL_MS6); -+ return () => { -+ gone = true; -+ clearInterval(timer); -+ }; -+ }, [live, editing]); -+ const retry = () => { -+ setPhase("loading"); -+ setError(""); -+ setOpened(null); -+ readWorkflows(invoke).then(apply2); -+ }; -+ const open = (item, source) => { -+ if (source === "factory") { -+ const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); -+ setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); -+ setEditing(false); -+ return; - } -+ setOpened({ source: "own", id: item.id }); -+ setEditing(false); - }; -- return (0, import_react43.createElement)( -- "div", -- { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react43.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), -- items.map((item) => (0, import_react43.createElement)( -- "div", -- { key: item.id, className: "bt-set-row" }, -- (0, import_react43.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), -- (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), -- (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) -- )) -- ); --} --function FaceMembers({ room: room2, roster, actions }) { -- const people2 = facePeople(room2, roster); -- const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); -- return (0, import_react43.createElement)( -- "div", -- null, -- (0, import_react43.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), -- (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react43.createElement)(FacePersonRow, { -- key: person.key, -- person, -- roster, -- className: "bt-member", -- onOpenBot: (id) => actions.openSession(id), -- lead: room2.face?.lead?.handle -- }))) -- ); --} --function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) { -- const live = room2.presentedParticipants ?? room2.participants ?? []; -- const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -- const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const { entries: entries2, now } = useMemberWork(room2, useWork); -- const invited = room2.viewerRole === "invited-owner"; -- const title = roomTitle(room2); -- const leaveMine = () => { -- const mine = remotes.find((p) => p.ownerId === roster.self?.memberId) || members.find((member) => member.ownerId === roster.self?.memberId); -- if (mine) void actions.updateRoom(room2.id, { removeParticipants: [{ kind: "bot", id: mine.handle || mine.id, remote: Boolean(mine.handle) }] }); -+ const useTemplate = (id) => { -+ const template = typeof id === "string" ? id : ""; -+ if (!template) return; -+ Promise.resolve(invoke("workflow/create", { template })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ const before = new Set((pack?.workflows ?? []).map((item) => item.id)); -+ const made = settled.pack.workflows.find((item) => !before.has(item.id)); -+ setPack(settled.pack); -+ setPhase(settled.phase); -+ setError(""); -+ if (made) { -+ setOpened({ source: "own", id: made.id }); -+ setEditing(true); -+ } -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not copy this workflow"))); -+ }); - }; -- const face = room2.origin === "face-to-face"; -- return [ -- (0, import_react43.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), -- invited && room2.sharedFrom && !face ? (0, import_react43.createElement)( -- "div", -- { key: "from", className: "bt-shared-from" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, title), -- (0, import_react43.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), -- room2.sharedFrom.invitedAt ? (0, import_react43.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, -- (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) -- ) : face ? null : room2.notice ? (0, import_react43.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react43.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), -- face ? null : (0, import_react43.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), -- face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react43.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react43.createElement)( -- "div", -- { key: "members" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), -- (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, [ -- ...humans.map((person) => (0, import_react43.createElement)( -- "div", -- { key: person.id, role: "listitem" }, -- (0, import_react43.createElement)( -- "div", -- { className: "bt-member" }, -- (0, import_react43.createElement)("span", { className: "bt-member-name" }, (0, import_react43.createElement)("span", null, person.displayName || person.id)), -- (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Human")), -- person.role === "admin" ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -- ) -- )), -- ...remotes.map((person) => (0, import_react43.createElement)( -- "div", -- { key: person.handle || person.id, role: "listitem" }, -- (0, import_react43.createElement)( -- "div", -- { className: "bt-member" }, -- (0, import_react43.createElement)( -- "span", -- { className: "bt-member-name" }, -- (0, import_react43.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -- (0, import_react43.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) -- ), -- face ? null : (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) -- ) -- )), -- ...members.map((member) => { -- const entry = entries2.find((item) => item.id === member.id); -- return (0, import_react43.createElement)( -- "div", -- { key: member.id, role: "listitem" }, -- (0, import_react43.createElement)( -- "div", -- { className: "bt-member" }, -- (0, import_react43.createElement)( -- "button", -- { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, -- (0, import_react43.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), -- (0, import_react43.createElement)("span", null, member.name) -- ), -- room2.admin === member.id ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -- ), -- entry ? (0, import_react43.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null -- ); -- }) -- ]) -- ), -- face ? null : (0, import_react43.createElement)( -- "div", -- { key: "mode" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Who answers")), -- (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) -- ), -- face || !usage ? null : (0, import_react43.createElement)( -- "div", -- { key: "usage" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")), -- (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, room2.name), (0, import_react43.createElement)("span", null, usage)) -- ) -- ]; --} --function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { -- const owner = roster.byId[sessionId]; -- const part = owner ? currentPart(owner) : sessionId; -- const isMainBot = isMainOf(roster, owner?.id); -- const [tab, setTab] = (0, import_react43.useState)("details"); -- const room2 = roster.roomsById[sessionId]; -- const [view, setView] = (0, import_react43.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); -- const [editing, setEditing] = (0, import_react43.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); -- const [draftName, setDraftName] = (0, import_react43.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); -- const [copied, setCopied] = (0, import_react43.useState)(false); -- const [error, setError] = (0, import_react43.useState)(""); -- const glide = useGlide(tab); -- const last = (0, import_react43.useRef)({ view, tab, move: void 0 }); -- if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; -- else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; -- const move = last.current.move; -- const side = move === "tab" ? void 0 : move; -- const close = () => actions.closeOverlay(); -- useEscape(close); -- const bot = owner; -- const running = useSessionStatus((map) => map.get(part)?.running === true); -- const doing = useActivity((map) => map[bot?.id ?? sessionId]); -- const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part], copyNames(roster))); -- const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); -- const run = (promise) => Promise.resolve(promise).then(() => { -- setError(""); -- return true; -- }).catch((failure) => { -- setError(failure?.message ?? String(failure)); -- return false; -- }); -- const share = () => { -- const text2 = transcript; -- if (!text2) { -- actions.notify?.(t("Nothing to copy yet")); -- return; -- } -- void copiedToClipboard(text2).then((written) => { -- if (!written) { -- actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); -+ const rename = (item) => { -+ setRenaming(item); -+ }; -+ const confirmRename = (workflowId, name) => { -+ if (!name.trim()) return; -+ Promise.resolve(invoke("workflow/rename", { workflowId, name: name.trim() })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); - return; - } -- setCopied(true); -- setTimeout(() => setCopied(false), 1600); -- actions.notify?.(t("Conversation copied as Markdown")); -+ setPack(settled.pack); -+ setPhase("ready"); -+ setError(""); -+ setRenaming(null); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not rename this workflow"))); - }); - }; -- const errorLine = error ? (0, import_react43.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -- if ((bot || room2) && view === "settings" && !bot?.remote) { -- return [ -- (0, import_react43.createElement)( -- "div", -- { key: "head", className: "bt-subhead", "data-move": move }, -- (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react43.createElement)(ChevronLeftIcon)), -- (0, import_react43.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), -- hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) -- ), -- (0, import_react43.createElement)( -- "div", -- { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, -- errorLine, -- bot ? (0, import_react43.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react43.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) -- ) -- ]; -- } -- const top = (0, import_react43.createElement)( -+ const deleteWorkflow = (item) => { -+ setDeleting(item); -+ }; -+ const confirmDelete = (workflowId) => { -+ Promise.resolve(invoke("workflow/delete", { workflowId })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ setPack(settled.pack); -+ setPhase(settled.phase); -+ setError(""); -+ setDeleting(null); -+ if (opened?.id === workflowId) setOpened(null); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not delete this workflow"))); -+ }); -+ }; -+ const commit = (graph) => { -+ if (!opened || opened.source !== "own") return; -+ Promise.resolve(invoke("workflow/graph", { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges })).then((value) => { -+ const settled = settleWorkflowRead(value); -+ if (settled.phase === "error") { -+ setError(settled.error); -+ return; -+ } -+ setPack(settled.pack); -+ setPhase("ready"); -+ setError(""); -+ }).catch((failure) => { -+ setError(workflowCallError(failure, t("Could not save this workflow"))); -+ }); -+ }; -+ return (0, import_react37.createElement)( - "div", -- { className: "bt-drawer-top", "data-move": side }, -- bot || room2 ? (0, import_react43.createElement)("button", { -- type: "button", -- className: "bt-round", -- "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -- title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -- "data-info-row": "settings", -- onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") -- }, (0, import_react43.createElement)(GearIcon)) : null, -- bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react43.createElement)(GlobeIcon)) : null, -- bot || room2 ? (0, import_react43.createElement)("button", { -- type: "button", -- className: "bt-round", -- // #1347: the accessible name says what the click does, which is copy — "Share conversation" -- // promised a sharing step that never came. It still says "Copied" after a copy, so a screen -- // reader hears the same confirmation the toast gives. -- "aria-label": copied ? t("Copied conversation") : t("Copy conversation"), -- title: copied ? t("Copied") : t("Copy conversation as Markdown"), -- onClick: share -- }, copied ? (0, import_react43.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react43.createElement)(ShareIcon)) : null, -- hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) -+ null, -+ (0, import_react37.createElement)(WorkflowsView, { -+ phase, -+ pack, -+ error, -+ opened, -+ editing, -+ roster, -+ onOpen: open, -+ onBack: () => { -+ setOpened(null); -+ setEditing(false); -+ }, -+ onEditing: setEditing, -+ onRetry: retry, -+ onUse: useTemplate, -+ onCommit: commit, -+ onRename: rename, -+ onDelete: deleteWorkflow -+ }), -+ renaming ? (0, import_react37.createElement)(RenameModal, { -+ workflow: renaming, -+ onClose: () => setRenaming(null), -+ onConfirm: (name) => confirmRename(renaming.id, name) -+ }) : null, -+ deleting ? (0, import_react37.createElement)(DeleteModal, { -+ workflow: deleting, -+ onClose: () => setDeleting(null), -+ onConfirm: () => confirmDelete(deleting.id) -+ }) : null - ); -- if (!bot && !room2) { -- return [ -- (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top), -- (0, import_react43.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react43.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) -- ]; -+} -+ -+// src/client/usage-data.js -+var HOUR = 0; -+var OWNER = 1; -+var MODEL = 2; -+var KIND = 3; -+var HOUR_MS = 36e5; -+var DAY_MS = 864e5; -+var RANGES = [ -+ { id: "24h", unit: "hour", count: 24, stride: 6 }, -+ { id: "7d", unit: "day", count: 7, stride: 1 }, -+ { id: "30d", unit: "day", count: 30, stride: 5 }, -+ { id: "12w", unit: "week", count: 12, stride: 2 }, -+ { id: "12m", unit: "month", count: 12, stride: 1 } -+]; -+var hourMs = (key) => Date.UTC(Number(key.slice(0, 4)), Number(key.slice(5, 7)) - 1, Number(key.slice(8, 10)), Number(key.slice(11, 13)) || 0); -+var dayKey = (ms) => new Date(ms).toISOString().slice(0, 10); -+var hourKey = (ms) => new Date(ms).toISOString().slice(0, 13).replace("T", " "); -+var mondayOf = (ms) => ms - (new Date(ms).getUTCDay() + 6) % 7 * DAY_MS; -+function spanRange(from, to) { -+ if (to < from) [from, to] = [to, from]; -+ const days = Math.round((hourMs(to) - hourMs(from)) / DAY_MS) + 1; -+ const unit = days <= 2 ? "hour" : days <= 62 ? "day" : days <= 182 ? "week" : "month"; -+ const range = { id: "custom", unit, from, to, days }; -+ const count = spanBuckets(range).length; -+ const stride = unit === "hour" ? 6 : [1, 2, 3, 5, 7, 10, 14].find((step) => count / step <= 12) ?? Math.ceil(count / 12); -+ return { ...range, count, stride }; -+} -+function spanBuckets(range) { -+ const first = hourMs(range.from); -+ const end = hourMs(range.to) + DAY_MS; -+ const buckets = []; -+ if (range.unit === "hour" || range.unit === "day") { -+ const step = range.unit === "hour" ? HOUR_MS : DAY_MS; -+ for (let start = first; start < end; start += step) buckets.push({ start, end: start + step, key: range.unit === "hour" ? hourKey(start) : dayKey(start) }); -+ } else if (range.unit === "week") { -+ for (let monday = mondayOf(first); monday < end; monday += 7 * DAY_MS) { -+ buckets.push({ start: Math.max(monday, first), end: Math.min(monday + 7 * DAY_MS, end), key: dayKey(monday) }); -+ } -+ } else { -+ const year = new Date(first).getUTCFullYear(); -+ for (let month = new Date(first).getUTCMonth(); Date.UTC(year, month, 1) < end; month += 1) { -+ const start = Date.UTC(year, month, 1); -+ buckets.push({ start: Math.max(start, first), end: Math.min(Date.UTC(year, month + 1, 1), end), key: dayKey(start).slice(0, 7) }); -+ } - } -- const current = bot?.name ?? room2.name; -- const saveName = async () => { -- setEditing(false); -- const next = draftName.trim(); -- if (next === current || room2 && !room2.named && next === "") return; -- if (bot && next) await run(actions.updateBot(bot.id, { name: next })); -- if (room2) await run(actions.updateRoom(room2.id, { name: next })); -+ return buckets; -+} -+function spanOf(range, now) { -+ if (range.from) { -+ return { buckets: spanBuckets(range), from: `${range.from} 00`, to: `${range.to} 23`, before: hourKey(hourMs(range.from) - range.days * DAY_MS) }; -+ } -+ const buckets = bucketsOf(range, now); -+ return { buckets: buckets.slice(range.count), from: hourKey(buckets[range.count].start), to: now, before: hourKey(buckets[0].start) }; -+} -+function bucketsOf(range, now) { -+ const end = hourMs(now); -+ const today = end - end % DAY_MS; -+ const buckets = []; -+ for (let back = range.count * 2 - 1; back >= 0; back -= 1) { -+ let start; -+ if (range.unit === "hour") start = end - back * HOUR_MS; -+ else if (range.unit === "day") start = today - back * DAY_MS; -+ else if (range.unit === "week") start = mondayOf(today) - back * 7 * DAY_MS; -+ else start = Date.UTC(new Date(end).getUTCFullYear(), new Date(end).getUTCMonth() - back, 1); -+ buckets.push({ start, key: range.unit === "hour" ? hourKey(start) : range.unit === "month" ? dayKey(start).slice(0, 7) : dayKey(start) }); -+ } -+ return buckets; -+} -+var tokensOf = (row) => row[4] + row[5] + row[6] + row[7]; -+var blank2 = () => ({ input: 0, cacheRead: 0, output: 0, calls: 0, total: 0 }); -+function addRow(sum, row) { -+ sum.input += row[4] + row[6]; -+ sum.cacheRead += row[5]; -+ sum.output += row[7]; -+ sum.calls += row[8]; -+ sum.total += tokensOf(row); -+ return sum; -+} -+function addTo(map, key, row) { -+ map.set(key, addRow(map.get(key) ?? blank2(), row)); -+} -+function summarize(view, range, keep = () => true, seriesOf = () => "all") { -+ const { buckets, from, to, before } = spanOf(range, view.now); -+ const index = new Map(buckets.map((bucket, at) => [bucket.key, at])); -+ const weeks = /* @__PURE__ */ new Map(); -+ const keyOf = (hour) => { -+ if (range.unit === "hour") return hour; -+ if (range.unit === "day") return hour.slice(0, 10); -+ if (range.unit === "month") return hour.slice(0, 7); -+ const day = hour.slice(0, 10); -+ let week = weeks.get(day); -+ if (week === void 0) weeks.set(day, week = dayKey(mondayOf(hourMs(day)))); -+ return week; - }; -- const subtitle = bot ? bot.role : roomSubtitle(room2, roster); -- const tabs = bot ? ["details", "browser"] : ["details"]; -- return [ -- (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top), -- (0, import_react43.createElement)( -- "div", -- { key: "id", className: "bt-drawer-id", "data-move": side }, -- bot ? (0, import_react43.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react43.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -- bot?.remote ? (0, import_react43.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react43.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react43.createElement)("input", { -- className: "bt-drawer-name-input", -- autoFocus: true, -- value: draftName, -- "aria-label": bot ? t("Bot name") : t("Group name"), -- placeholder: room2 ? t("Name after members") : void 0, -- onChange: (event) => setDraftName(event.target.value), -- onBlur: saveName, -- onKeyDown: (event) => { -- if (event.key === "Enter") void saveName(); -- if (event.key === "Escape") { -- event.stopPropagation(); -- setEditing(false); -- } -- } -- }) : (0, import_react43.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { -- setDraftName(room2 && !room2.named ? "" : current); -- setEditing(true); -- } }, current), -- subtitle ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null -- ), -- bot?.remote ? null : (0, import_react43.createElement)( -- "div", -- { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, -- (0, import_react43.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- tabs.map((key) => (0, import_react43.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) -- ), -- (0, import_react43.createElement)( -- "div", -- { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, -- errorLine, -- room2 && tab === "details" ? (0, import_react43.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, -- bot && bot.remote && tab === "details" ? [ -- bot.remote.handle ? (0, import_react43.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, -- (0, import_react43.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) -- ] : null, -- bot && !bot.remote && tab === "details" ? [ -- (0, import_react43.createElement)( -- "div", -- { key: "model", className: "bt-drawer-model" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Model")), -- (0, import_react43.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), -- modelOf(bot, roster) ? null : (0, import_react43.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) -- ), -- (0, import_react43.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), -- (0, import_react43.createElement)( -- "div", -- { key: "folder", className: "bt-drawer-model" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Working folder")), -- (0, import_react43.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) -- ), -- bot.instructions ? (0, import_react43.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react43.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), -- roster.memory === false ? null : (0, import_react43.createElement)(MemoryRow, { key: "memory", bot, actions }), -- usage ? (0, import_react43.createElement)( -- "div", -- { key: "usage" }, -- (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")), -- (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, bot.name), (0, import_react43.createElement)("span", null, usage)) -- ) : null, -- (0, import_react43.createElement)(ScheduleSummary, { key: "routines", bot, actions }) -- ] : null, -- bot && !bot.remote && tab === "browser" ? (0, import_react43.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null -- ) -- ]; -+ const current = buckets.map((bucket) => ({ ...bucket, sum: blank2(), stacks: /* @__PURE__ */ new Map() })); -+ const result = { buckets: current, total: blank2(), previous: 0, owners: /* @__PURE__ */ new Map(), models: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map() }; -+ for (const row of view.rows) { -+ const hour = row[HOUR]; -+ if (hour < before || hour > to || !keep(row)) continue; -+ if (hour < from) { -+ result.previous += tokensOf(row); -+ continue; -+ } -+ const at = index.get(keyOf(hour)); -+ if (at === void 0) continue; -+ const bucket = current[at]; -+ addRow(bucket.sum, row); -+ addRow(result.total, row); -+ const series = seriesOf(row); -+ bucket.stacks.set(series, (bucket.stacks.get(series) ?? 0) + tokensOf(row)); -+ addTo(result.owners, row[OWNER], row); -+ addTo(result.models, row[MODEL], row); -+ addTo(result.kinds, row[KIND], row); -+ } -+ return result; -+} -+function trimmed2(value) { -+ const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); -+ return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; -+} -+function compact(count) { -+ for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { -+ if (count >= size * 0.9995) return `${trimmed2(count / size)}${unit}`; -+ } -+ return String(Math.round(count)); -+} -+function percent(part, whole) { -+ if (!(whole > 0) || !(part > 0)) return "0"; -+ const value = part / whole * 100; -+ return value < 1 ? "<1" : String(Math.round(value)); - } - --// src/client/browser-pane.js --var import_react44 = require("react"); --var import_react_dom10 = require("react-dom"); --var PANEL_LINGER_MS = 300; --var WIDTH_KEY = STORAGE.browserWidth; --var READ_FLASH_MS = 2400; --var EMPTY_PAGE = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, excerpt: "" }; --var EMPTY_TABS2 = { ids: [], active: null }; --var SPRING_LEAD2 = springEasing(576, 39.4); --var SPRING_TRAIL2 = springEasing(144, 19.7); --var icon2 = (paths) => function BrowserIcon() { -- return (0, import_react44.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 }, -- paths.map((d, index) => (0, import_react44.createElement)("path", { key: index, d })) -- ); -+// src/client/usage-page.js -+var import_react38 = require("react"); -+var HEAT_WEEKS = 53; -+var TOP_SERIES = 7; -+var RING_R = 42; -+var RING2 = 2 * Math.PI * RING_R; -+var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; -+var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; -+var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -+var PARTS = [["output", 1], ["input", 0.6], ["cacheRead", 0.32]]; -+var partLabel = (part) => ({ output: t("Output"), input: t("Input"), cacheRead: t("Cache hit") })[part]; -+var soften = (paint, strength = 1) => { -+ const mix = (color) => `color-mix(in srgb, ${color} ${Math.round(88 * strength)}%, var(--bt-card))`; -+ return paint.startsWith("linear-gradient(") ? paint.replace(/#[0-9a-f]{6}/gi, mix) : mix(paint); - }; --var GlobeIcon = icon2(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); --var BackIcon = icon2(["M12.5 4.5L7 10l5.5 5.5"]); --var ForwardIcon = icon2(["M7.5 4.5L13 10l-5.5 5.5"]); --var ReloadIcon = icon2(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); --var StopIcon = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); --var CloseIcon2 = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); --var PlusTabIcon = icon2(["M10 5v10M5 10h10"]); --var LockIcon2 = icon2(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); --function Favicon({ src, className }) { -- const [failed, setFailed] = (0, import_react44.useState)(null); -- if (!src || failed === src) return (0, import_react44.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react44.createElement)(GlobeIcon)); -- return (0, import_react44.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); -+var full = (count) => new Intl.NumberFormat(dateLocale()).format(Math.round(count)); -+var hitRate = (sum) => percent(sum.cacheRead, sum.input + sum.cacheRead); -+var dates = (options) => new Intl.DateTimeFormat(dateLocale(), { timeZone: "UTC", ...options }); -+var hourText = (ms) => `${String(new Date(ms).getUTCHours()).padStart(2, "0")}:00`; -+var yearOf = (ms) => new Date(ms).getUTCFullYear(); -+function spanText(from, to) { -+ const start = hourMs(from); -+ const end = hourMs(to); -+ const year = (/* @__PURE__ */ new Date()).getFullYear(); -+ const format = dates(yearOf(start) !== year || yearOf(end) !== year ? { year: "numeric", month: "short", day: "numeric" } : { month: "short", day: "numeric" }); -+ return from === to ? format.format(start) : `${format.format(start)} – ${format.format(end)}`; - } --var savedWidth = () => { -- try { -- const value = Number(localStorage.getItem(WIDTH_KEY)); -- return Number.isFinite(value) && value >= MIN_DETAILS ? value : null; -- } catch { -- return null; -+var daysText = (count) => count === 1 ? t("1 day") : t("{count} days", { count }); -+var lastText = (range) => range.from ? spanText(range.from, range.to) : t("Last {range}", { range: rangeLabel(range.id) }); -+function compareText(range) { -+ if (!range.from) return t("Compared with the {range} before", { range: rangeLabel(range.id) }); -+ return range.days === 1 ? t("Compared with the day before") : t("Compared with the {count} days before", { count: range.days }); -+} -+function tickOf(range, bucket) { -+ if (range.unit === "hour") { -+ return range.days > 1 && new Date(bucket.start).getUTCHours() === 0 ? dates({ month: "numeric", day: "numeric" }).format(bucket.start) : hourText(bucket.start); - } --}; --var fitWidth = (width, [low, high]) => Math.round(Math.max(low, Math.min(width, high))); --var widthRange = (view, viewport) => panelWidthRange(view, viewport); --var room = { rule: null, selector: "" }; --var roomRule = () => { -- const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -- if (live && room.selector) return room.rule; -- const column = document.querySelector('[class*="_centerCol"]'); -- const selector = column ? [...column.classList].filter((name) => name.includes("_centerCol")).map((name) => `.${CSS.escape(name)}`).join(",") : ""; -- if (live && !selector) return room.rule; -- const declarations2 = room.rule?.style.cssText ?? ""; -- room.rule?.parentStyleSheet?.ownerNode?.remove(); -- const style = document.createElement("style"); -- style.dataset.dshBot = "chat-room"; -- style.textContent = `${selector || '[class*="_centerCol"]'}{${declarations2}}`; -- document.head.append(style); -- room.rule = style.sheet.cssRules[0]; -- room.selector = selector; -- return room.rule; --}; --var roomHold = 0; --var chatRoom = { -- /** The column's right margin in px, or null for the shell's own. */ -- set(width) { -- const { style } = roomRule(); -- if (width === null) style.removeProperty("margin-right"); -- else style.setProperty("margin-right", `${width}px`, "important"); -- }, -- /** While on, a margin change lands at once instead of gliding. */ -- hold(on) { -- cancelAnimationFrame(roomHold); -- if (on) { -- roomRule().style.setProperty("transition", "none", "important"); -- return; -+ if (range.unit === "month") return dates({ month: "short" }).format(bucket.start); -+ return dates({ month: "numeric", day: "numeric" }).format(bucket.start); -+} -+function titleOf(range, bucket) { -+ if (range.unit === "hour") return `${dates({ month: "short", day: "numeric" }).format(bucket.start)} ${hourText(bucket.start)}–${hourText(bucket.start + HOUR_MS)}`; -+ if (range.unit === "day") return dates({ month: "long", day: "numeric", weekday: "short" }).format(bucket.start); -+ if (range.unit === "week") return spanText(dayKey(bucket.start), dayKey((bucket.end ?? bucket.start + 7 * DAY_MS) - DAY_MS)); -+ if (bucket.end && (new Date(bucket.start).getUTCDate() !== 1 || new Date(bucket.end).getUTCDate() !== 1)) return spanText(dayKey(bucket.start), dayKey(bucket.end - DAY_MS)); -+ return dates({ year: "numeric", month: "long" }).format(bucket.start); -+} -+function useTween(value) { -+ const [shown, setShown] = (0, import_react38.useState)(0); -+ const from = (0, import_react38.useRef)(0); -+ (0, import_react38.useEffect)(() => { -+ if (reducedMotion() || typeof requestAnimationFrame !== "function") { -+ from.current = value; -+ setShown(value); -+ return void 0; - } -- roomHold = requestAnimationFrame(() => { -- roomHold = requestAnimationFrame(() => roomRule().style.removeProperty("transition")); -- }); -- }, -- /** The columns themselves, for a drag's per-frame writes. */ -- columns() { -- roomRule(); -- return room.selector ? [...document.querySelectorAll(room.selector)] : []; -- } --}; --var shortAddress = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, ""); --var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; --var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); --function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { -- const pages = useBrowserPages((map) => map); -- const wrap = (0, import_react44.useRef)(null); -- const ind = (0, import_react44.useRef)(null); -- const spot = (0, import_react44.useRef)(null); -- const under = (0, import_react44.useRef)(null); -- (0, import_react44.useLayoutEffect)(() => { -- const box = wrap.current; -- const capsule = ind.current; -- if (!box || !capsule) return void 0; -- const node = view === "details" ? box.querySelector(".bt-ptab-bot") : box.querySelector(`[data-tab="${active}"]`); -- under.current = node; -- if (!node) { -- capsule.style.opacity = "0"; -- spot.current = null; -- return void 0; -- } -- if (view === "details") box.scrollLeft = 0; -- else node.scrollIntoView?.({ block: "nearest", inline: "nearest" }); -- const to = { left: node.offsetLeft, right: box.offsetWidth - node.offsetLeft - node.offsetWidth }; -- const from = spot.current; -- spot.current = to; -- capsule.style.opacity = "1"; -- capsule.style.left = `${to.left}px`; -- capsule.style.right = `${to.right}px`; -- if (from === null || from.left === to.left && from.right === to.right || reducedMotion2()) return void 0; -- const ahead = to.left > from.left ? "right" : "left"; -- const behind = ahead === "right" ? "left" : "right"; -- const lead = capsule.animate([{ [ahead]: `${from[ahead]}px` }, { [ahead]: `${to[ahead]}px` }], { duration: SPRING_LEAD2.duration, easing: SPRING_LEAD2.easing }); -- const trail = capsule.animate([{ [behind]: `${from[behind]}px` }, { [behind]: `${to[behind]}px` }], { duration: SPRING_TRAIL2.duration, easing: SPRING_TRAIL2.easing }); -- return () => { -- lead.cancel(); -- trail.cancel(); -+ const begin = from.current; -+ const started = performance.now(); -+ let frame; -+ const step = (now) => { -+ const progress = Math.min(1, (now - started) / 650); -+ from.current = begin + (value - begin) * (1 - (1 - progress) ** 3); -+ setShown(from.current); -+ if (progress < 1) frame = requestAnimationFrame(step); - }; -- }); -- (0, import_react44.useEffect)(() => { -- const box = wrap.current; -- if (!box) return void 0; -- const observer = new ResizeObserver(() => { -- const node = under.current; -- const capsule = ind.current; -- if (!node?.isConnected || !capsule || spot.current === null) return; -- spot.current = { left: node.offsetLeft, right: box.offsetWidth - node.offsetLeft - node.offsetWidth }; -- capsule.style.left = `${spot.current.left}px`; -- capsule.style.right = `${spot.current.right}px`; -- }); -- observer.observe(box); -- return () => observer.disconnect(); -- }, []); -- return (0, import_react44.createElement)( -- "div", -- { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, -- (0, import_react44.createElement)( -- "div", -- { className: "bt-ptabs-in", ref: wrap }, -- (0, import_react44.createElement)( -- "button", -- { -- type: "button", -- role: "tab", -- "aria-selected": view === "details", -- className: "bt-ptab bt-ptab-bot", -- title: bot.name, -- onClick: () => onView("details") -- }, -- (0, import_react44.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), -- view === "details" ? (0, import_react44.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null -- ), -- ids.map((tabId) => { -- const page = pages[tabId] ?? EMPTY_PAGE; -- const on = view === "browser" && tabId === active; -- return (0, import_react44.createElement)( -- "button", -- { -- key: tabId, -- type: "button", -- role: "tab", -- "aria-selected": on, -- "data-tab": tabId, -- className: "bt-ptab", -- title: page.url || t("New tab"), -- onClick: () => { -- actions.browserActivate(bot.id, tabId); -- onView("browser"); -- }, -- onAuxClick: (event) => { -- if (event.button === 1) { -- event.preventDefault(); -- void actions.browserCloseTab(bot.id, tabId); -- } -- } -- }, -- (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), -- (0, import_react44.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), -- (0, import_react44.createElement)("span", { -- className: "bt-ptab-x", -- role: "button", -- "aria-label": t("Close tab"), -- title: t("Close tab"), -- onClick: (event) => { -- event.stopPropagation(); -- void actions.browserCloseTab(bot.id, tabId); -- } -- }, "×") -- ); -- }), -- (0, import_react44.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) -- ), -- // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. -- (0, import_react44.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react44.createElement)(PlusTabIcon)), -- (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react44.createElement)(CloseIcon2)) -- ); -+ frame = requestAnimationFrame(step); -+ return () => cancelAnimationFrame(frame); -+ }, [value]); -+ return shown; - } --function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { -- const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE; -- const frame = (0, import_react44.useRef)(null); -- const input = (0, import_react44.useRef)(null); -- const [draft, setDraft] = (0, import_react44.useState)(null); -- const showGuest = landed && !leaving && Boolean(page.url) && !page.error; -- (0, import_react44.useLayoutEffect)(() => { -- const element = frame.current; -- if (!element || leaving || !tabId) { -- void actions.browserShow(null); -- return void 0; -- } -- let raf = 0; -- const measure = () => { -- cancelAnimationFrame(raf); -- raf = 0; -- const box = element.getBoundingClientRect(); -- void actions.browserShow(tabId, showGuest ? { left: box.left, top: box.top, width: box.width, height: box.height } : null); -+var glyphBox = (size, icon3) => (0, import_react38.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react38.createElement)(icon3)); -+function ModelGlyph({ name, paint, size }) { -+ return (0, import_react38.createElement)("span", { -+ className: "bt-us-model", -+ "aria-hidden": true, -+ style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -+ }, (name.match(/[\p{L}\p{N}]/u)?.[0] ?? "?").toUpperCase()); -+} -+function ownerFace(owner, roster) { -+ if (owner.kind === "bot") { -+ const bot = roster.byId[owner.id] ?? { id: owner.id, name: owner.name }; -+ const name = bot.name || owner.name || t("Deleted Bot"); -+ const role = bot.role?.trim() ?? ""; -+ return { -+ kind: "bot", -+ name, -+ sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, -+ paint: paintCss(colorOf(bot)), -+ stops: GRADIENTS[colorOf(bot)], -+ glyph: (size) => (0, import_react38.createElement)(BotAvatar, { bot, size, badge: false, live: false }) - }; -- const schedule = () => { -- if (!raf) raf = requestAnimationFrame(measure); -+ } -+ if (owner.kind === "room") { -+ const room2 = roster.roomsById[owner.id]; -+ return { -+ kind: "room", -+ name: owner.name || t("Deleted group chat"), -+ sub: owner.deleted ? t("Deleted") : t("Group chat"), -+ paint: "var(--bt-us-rest)", -+ glyph: (size) => room2 ? (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) - }; -- measure(); -- const observer = new ResizeObserver(measure); -- observer.observe(element); -- window.addEventListener("resize", schedule); -- return () => { -- cancelAnimationFrame(raf); -- observer.disconnect(); -- window.removeEventListener("resize", schedule); -+ } -+ if (owner.kind === "agent") { -+ return { -+ kind: "agent", -+ name: owner.name || "DSH Agent", -+ sub: owner.deleted ? t("Deleted") : "", -+ paint: "var(--bt-us-rest)", -+ glyph: (size) => glyphBox(size, ChatsIcon) - }; -- }, [tabId, showGuest, leaving]); -- (0, import_react44.useEffect)(() => { -- setDraft(null); -- if (landed && !page.url) input.current?.focus(); -- }, [tabId, landed]); -- const go = (event) => { -- event.preventDefault(); -- const text2 = draft ?? ""; -- if (text2.trim() === "") return; -- setDraft(null); -- input.current?.blur(); -- void actions.browserNavigate(tabId, text2).then((opened) => { -- if (opened) actions.browserFocus(tabId); -- }); -- }; -- const editing = draft !== null; -- const secure = /^https:/i.test(page.url); -- return (0, import_react44.createElement)( -+ } -+ return { kind: "other", name: t("Other"), sub: "", paint: "var(--bt-us-rest)", glyph: (size) => glyphBox(size, ChatsIcon) }; -+} -+function kindLabel(kind, botFocus) { -+ const labels = { chat: botFocus ? t("Own chat") : t("Conversations"), group: t("Group chats"), compaction: t("Compaction and handoff"), image: t("Image reading"), memory: t("Memory"), "session-title": t("Titles"), other: t("Other") }; -+ return labels[kind] ?? kind; -+} -+function Tabs({ label, value, options, onChange, boxRef }) { -+ const names = options.map(([, text2]) => text2).join("\n"); -+ const glide = useGlide(`${value} -+${names}`, { box: boxRef }); -+ return (0, import_react38.createElement)( - "div", -- { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, -- (0, import_react44.createElement)( -- "div", -- { className: "bt-browser-bar" }, -- (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react44.createElement)(BackIcon)), -- (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react44.createElement)(ForwardIcon)), -- (0, import_react44.createElement)("button", { -- type: "button", -- className: "bt-icon-btn", -- disabled: !page.url, -- "aria-label": page.loading ? t("Stop") : t("Reload"), -- title: page.loading ? t("Stop") : t("Reload"), -- onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") -- }, (0, import_react44.createElement)(page.loading ? StopIcon : ReloadIcon)), -- (0, import_react44.createElement)( -- "form", -- { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, -- (0, import_react44.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react44.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), -- (0, import_react44.createElement)("input", { -- ref: input, -- type: "text", -- spellCheck: false, -- autoComplete: "off", -- "aria-label": t("Address"), -- placeholder: t("Search or enter address"), -- value: editing ? draft : shortAddress(page.url), -- onFocus: (event) => { -- setDraft(page.url); -- const target = event.target; -- requestAnimationFrame(() => target.select()); -- }, -- onBlur: () => setDraft(null), -- onChange: (event) => setDraft(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setDraft(null); -- event.currentTarget.blur(); -- } -- } -- }) -- ), -- page.loading ? (0, import_react44.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null -- ), -- (0, import_react44.createElement)( -- "div", -- { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, -- page.error ? (0, import_react44.createElement)( -- "div", -- { className: "bt-browser-empty" }, -- (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), -- (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), -- page.url ? (0, import_react44.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { -- void actions.browserNavigate(tabId, page.url); -- } }, t("Try again")) : null -- ) : !page.url ? (0, import_react44.createElement)( -- "div", -- { className: "bt-browser-empty" }, -- (0, import_react44.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), -- (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -- (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) -- ) : null -- ) -+ { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, -+ (0, import_react38.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.map(([id, text2, icon3]) => (0, import_react38.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react38.createElement)(icon3) : null, text2)) - ); - } --function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { -- const bot = roster.byId[pane.id]; -- const canBrowse = Boolean(bot && roster.browser === true && actions.browserShow); -- const view = pane.view === "browser" && canBrowse ? "browser" : "details"; -- const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); -- const active = tabState.active; -- const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); -- const root = (0, import_react44.useRef)(null); -- const fly = (0, import_react44.useRef)(null); -- const [landed, setLanded] = (0, import_react44.useState)(false); -- const [settling, setSettling] = (0, import_react44.useState)(false); -- const [width, setWidth] = (0, import_react44.useState)(savedWidth); -- const [viewport, setViewport] = (0, import_react44.useState)(() => window.innerWidth); -- const [dragging, setDragging] = (0, import_react44.useState)(false); -- const [resizing, setResizing] = (0, import_react44.useState)(false); -- const instant = dragging || resizing; -- const [reading, setReading] = (0, import_react44.useState)(false); -- const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); -- (0, import_react44.useLayoutEffect)(() => { -- if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); -- }, [view, width]); -- (0, import_react44.useLayoutEffect)(() => { -- chatRoom.hold(instant); -- }, [instant]); -- const [last, setLast] = (0, import_react44.useState)({ view, width: panelWidth }); -- if (last.view !== view || last.width !== panelWidth) { -- setLast({ view, width: panelWidth }); -- if (last.view !== view) setSettling(last.width !== panelWidth && !instant); -- } -- const shownView = (0, import_react44.useRef)(view); -- (0, import_react44.useLayoutEffect)(() => { -- const switched = settling && shownView.current !== view; -- shownView.current = view; -- if (switched) chatRoom.hold(true); -- chatRoom.set(leaving ? null : panelWidth); -- if (switched) chatRoom.hold(false); -- }, [view, panelWidth, leaving]); -- (0, import_react44.useEffect)(() => () => { -- chatRoom.set(null); -- chatRoom.hold(false); -- }, []); -- (0, import_react44.useEffect)(() => { -- let timer; -- const onResize = () => { -- setResizing(true); -- setViewport(window.innerWidth); -- clearTimeout(timer); -- timer = setTimeout(() => setResizing(false), 160); -+function Stats({ range, total, previous }) { -+ const shown = useTween(total.total); -+ const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; -+ const cell = (label, value) => (0, import_react38.createElement)("div", { className: "bt-us-stat" }, (0, import_react38.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react38.createElement)("span", { className: "bt-us-stat-value" }, value)); -+ return (0, import_react38.createElement)( -+ "section", -+ { className: "bt-us-stats", style: { "--i": 0 } }, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-stat bt-us-stat-main" }, -+ (0, import_react38.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), -+ (0, import_react38.createElement)( -+ "span", -+ { className: "bt-us-stat-value" }, -+ compact(shown), -+ (0, import_react38.createElement)("small", null, t("tokens")), -+ change === null ? null : (0, import_react38.createElement)( -+ "span", -+ { className: "bt-us-delta", title: compareText(range) }, -+ `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -+ ) -+ ) -+ ), -+ cell(t("Cache hit rate"), `${hitRate(total)}%`), -+ cell(t("Output"), compact(total.output)), -+ cell(t("Calls"), full(total.calls)) -+ ); -+} -+function niceTop(max) { -+ if (!(max > 0)) return 1; -+ const step = 10 ** Math.floor(Math.log10(max)); -+ return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); -+} -+function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { -+ const [hover, setHover] = (0, import_react38.useState)(null); -+ const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); -+ const count = columns.length; -+ const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); -+ const tipOf = new Map(tipSeries.map((entry) => [entry.key, entry])); -+ const tip = hover === null ? null : columns[hover]; -+ const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; -+ return (0, import_react38.createElement)( -+ "section", -+ { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-card-head" }, -+ (0, import_react38.createElement)("h3", null, t("Tokens over time"), (0, import_react38.createElement)("span", null, unitLabel(range.unit))), -+ onBy ? (0, import_react38.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null -+ ), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-chart-body" }, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, -+ [1, 0.5].map((level) => (0, import_react38.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react38.createElement)("span", null, compact(top * level)))), -+ (0, import_react38.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react38.createElement)("span", null, "0")), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-cols" }, -+ columns.map((column, at) => (0, import_react38.createElement)("div", { -+ key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, -+ className: "bt-us-col", -+ "data-on": hover === at ? "" : void 0, -+ onMouseEnter: () => setHover(at) -+ }, column.total > 0 ? (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, -+ column.parts.map(([key, value]) => value > 0 ? (0, import_react38.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) -+ ) : (0, import_react38.createElement)("div", { className: "bt-us-stub" }))) -+ ), -+ tip ? (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-tip", style: tipSide }, -+ (0, import_react38.createElement)("div", { className: "bt-us-tip-title" }, tip.title), -+ (0, import_react38.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), -+ tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react38.createElement)( -+ "div", -+ { key, className: "bt-us-tip-row" }, -+ (0, import_react38.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), -+ (0, import_react38.createElement)("span", null, tipOf.get(key)?.name), -+ (0, import_react38.createElement)("b", null, compact(value)) -+ )) -+ ) : null -+ ), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-ticks", "aria-hidden": true }, -+ // A preset counts its labels back from now; a picked span forward from its first day. -+ columns.map((column, at) => (0, import_react38.createElement)( -+ "span", -+ { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, -+ (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" -+ )) -+ ) -+ ), -+ series.length > 1 || series[0]?.pick ? (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-legend" }, -+ series.map((entry) => (0, import_react38.createElement)(entry.pick ? "button" : "span", { -+ key: entry.key, -+ className: "bt-us-key", -+ ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} -+ }, (0, import_react38.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) -+ ) : null -+ ); -+} -+function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick }) { -+ const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; -+ return (0, import_react38.createElement)( -+ onClick ? "button" : "div", -+ { -+ className: "bt-us-row", -+ "data-kind": kind, -+ style: { "--i": index }, -+ title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), -+ ...onClick ? { type: "button", onClick } : {} -+ }, -+ glyph ? (0, import_react38.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, -+ (0, import_react38.createElement)( -+ "span", -+ { className: "bt-us-row-body" }, -+ (0, import_react38.createElement)( -+ "span", -+ { className: "bt-us-row-line" }, -+ (0, import_react38.createElement)("span", { className: "bt-us-row-name" }, name), -+ sub ? (0, import_react38.createElement)("span", { className: "bt-us-row-sub" }, sub) : null -+ ), -+ (0, import_react38.createElement)("span", { className: "bt-us-track" }, (0, import_react38.createElement)("span", { style: { width: `${width}%`, background: fill3 } })) -+ ), -+ (0, import_react38.createElement)("span", { className: "bt-us-row-num" }, (0, import_react38.createElement)("b", null, compact(sum.total)), (0, import_react38.createElement)("small", null, `${percent(sum.total, whole)}%`)) -+ ); -+} -+function ListCard({ title, count, index, children }) { -+ return (0, import_react38.createElement)( -+ "section", -+ { className: "bt-us-card", style: { "--i": index } }, -+ (0, import_react38.createElement)("div", { className: "bt-us-card-head" }, (0, import_react38.createElement)("h3", null, title, count === void 0 ? null : (0, import_react38.createElement)("span", null, count))), -+ (0, import_react38.createElement)("div", { className: "bt-us-rows" }, children) -+ ); -+} -+function Profile({ face, share, range }) { -+ const arc = Math.max(0, Math.min(1, share)) * RING2; -+ return (0, import_react38.createElement)( -+ "section", -+ { className: "bt-us-profile" }, -+ (0, import_react38.createElement)( -+ "span", -+ { className: "bt-us-ring" }, -+ (0, import_react38.createElement)( -+ "svg", -+ { viewBox: "0 0 96 96", "aria-hidden": true }, -+ face.stops ? (0, import_react38.createElement)("defs", null, (0, import_react38.createElement)( -+ "linearGradient", -+ { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, -+ face.stops.map(([offset, color]) => (0, import_react38.createElement)("stop", { key: offset, offset, stopColor: color })) -+ )) : null, -+ (0, import_react38.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), -+ arc > 0 ? (0, import_react38.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null -+ ), -+ (0, import_react38.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) -+ ), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-profile-copy" }, -+ (0, import_react38.createElement)("h3", null, face.name), -+ face.sub ? (0, import_react38.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, -+ (0, import_react38.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) -+ ) -+ ); -+} -+var monthOf = (ms) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth(), 1); -+var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); -+function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { -+ const box = (0, import_react38.useRef)(null); -+ const todayMs = hourMs(today); -+ const thisMonth = monthOf(todayMs); -+ const [right, setRight] = (0, import_react38.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); -+ const [anchor, setAnchor] = (0, import_react38.useState)(null); -+ const [hover, setHover] = (0, import_react38.useState)(null); -+ (0, import_react38.useEffect)(() => { -+ const onDown = (event) => { -+ if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); - }; -- window.addEventListener("resize", onResize); -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ onClose(); -+ } -+ }; -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); - return () => { -- clearTimeout(timer); -- window.removeEventListener("resize", onResize); -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); - }; - }, []); -- (0, import_react44.useEffect)(() => { -- if (!settling) return void 0; -- const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); -- return () => clearTimeout(timer); -- }, [settling, view]); -- (0, import_react44.useEffect)(() => { -- const timer = setTimeout(() => setLanded(true), 360); -- return () => clearTimeout(timer); -- }, []); -- (0, import_react44.useEffect)(() => { -- if (canBrowse) actions.browserEnsure(bot.id); -- }, [bot?.id, canBrowse]); -- (0, import_react44.useEffect)(() => { -- if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); -- }, [view, active, tabState.ids.length]); -- (0, import_react44.useEffect)(() => { -- if (view !== "browser" || leaving) void actions.browserShow?.(null); -- }, [view, leaving]); -- (0, import_react44.useEffect)(() => () => { -- void actions.browserShow?.(null); -- }, []); -- (0, import_react44.useEffect)(() => { -- if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; -- setReading(true); -- const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -- return () => clearTimeout(timer); -- }, [page.readAt]); -- const beginSwitch = (next) => { -- if (reducedMotion2()) return; -- const panel = root.current; -- const outgoing = panel?.querySelector(".bt-panel-view"); -- if (outgoing) { -- const ghostEl = document.createElement("div"); -- ghostEl.className = "bt-panel-ghost"; -- ghostEl.style.width = `${outgoing.offsetWidth}px`; -- ghostEl.append(outgoing.cloneNode(true)); -- outgoing.parentElement.append(ghostEl); -- void ghostEl.animate([ -- { opacity: 1, transform: "scale(1)" }, -- { opacity: 0, transform: "scale(0.985)" } -- ], { duration: 160, easing: "ease-out", fill: "forwards" }).finished.catch(() => { -- }).finally(() => ghostEl.remove()); -+ const [low, high] = anchor ? [anchor, hover ?? anchor].sort() : span ? [span.from, span.to] : [null, null]; -+ const press = (key) => { -+ if (!anchor) { -+ setAnchor(key); -+ return; - } -- const source = next === "browser" ? panel?.querySelector(".bt-drawer-id .bt-mark") : panel?.querySelector(".bt-ptab-bot .bt-mark"); -- if (source) fly.current = { node: source.cloneNode(true), from: source.getBoundingClientRect() }; -- }; -- const switchView = (next) => { -- if (next === view || leaving) return; -- beginSwitch(next); -- actions.setPanelView(next); -+ const [from, to] = [anchor, key].sort(); -+ onPick(from, to); - }; -- const panelActions = { ...actions }; -- for (const name of ["openBrowser", "browserNewTab", "browserPick"]) { -- if (actions[name]) panelActions[name] = (...args) => { -- if (view !== "browser") beginSwitch("browser"); -- return actions[name](...args); -- }; -- } -- (0, import_react44.useLayoutEffect)(() => { -- const move = fly.current; -- if (!move) return; -- fly.current = null; -- const target = view === "browser" ? root.current?.querySelector(".bt-ptab-bot .bt-mark") : root.current?.querySelector(".bt-drawer-id .bt-mark"); -- if (!target || move.from.width === 0) return; -- const growing = root.current.getAnimations({ subtree: true }).filter((animation) => animation.animationName === "bt-strip-in"); -- const at = growing.map((animation) => animation.currentTime); -- for (const animation of growing) animation.currentTime = animation.effect.getComputedTiming().endTime; -- const to = target.getBoundingClientRect(); -- growing.forEach((animation, index) => { -- animation.currentTime = at[index]; -- }); -- const clone = move.node; -- Object.assign(clone.style, { -- position: "fixed", -- left: `${move.from.left}px`, -- top: `${move.from.top}px`, -- width: `${move.from.width}px`, -- height: `${move.from.height}px`, -- margin: "0", -- transformOrigin: "0 0", -- zIndex: "80", -- pointerEvents: "none", -- visibility: "" -- }); -- document.body.append(clone); -- target.style.visibility = "hidden"; -- const scale = to.width / move.from.width; -- const anim = clone.animate([ -- { transform: "translate(0px, 0px) scale(1)" }, -- { transform: `translate(${to.left - move.from.left}px, ${to.top - move.from.top}px) scale(${scale})` } -- ], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, fill: "forwards" }); -- void anim.finished.catch(() => { -- }).finally(() => { -- clone.remove(); -- target.style.visibility = ""; -- }); -- }, [view, pane.id]); -- const startResize = (event) => { -- if (event.button !== 0 || leaving) return; -- event.preventDefault(); -- const panel = root.current; -- const grip = event.currentTarget; -- grip.setPointerCapture?.(event.pointerId); -- const range = widthRange(view, window.innerWidth); -- const from = { x: event.clientX, width: panelWidth }; -- let next = panelWidth; -- setDragging(true); -- setSettling(false); -- chatRoom.hold(true); -- const columns = chatRoom.columns(); -- for (const column of columns) column.style.setProperty("transition", "none", "important"); -- const onMove = (move) => { -- next = fitWidth(from.width + from.x - move.clientX, range); -- panel.style.width = `${next}px`; -- if (columns.length === 0) chatRoom.set(next); -- for (const column of columns) column.style.setProperty("margin-right", `${next}px`, "important"); -- }; -- const ends = ["pointerup", "pointercancel", "lostpointercapture"]; -- const onEnd = (end) => { -- if (end.type === "pointerup") onMove(end); -- grip.removeEventListener("pointermove", onMove); -- for (const name of ends) grip.removeEventListener(name, onEnd); -- chatRoom.set(next); -- for (const column of columns) { -- column.style.removeProperty("margin-right"); -- column.style.removeProperty("transition"); -- } -- panel.style.setProperty("--bt-panel-w", `${next}px`); -- panel.style.removeProperty("width"); -- setDragging(false); -- if (next === from.width) return; -- setWidth(next); -- try { -- localStorage.setItem(WIDTH_KEY, String(next)); -- } catch { -- } -- }; -- grip.addEventListener("pointermove", onMove); -- for (const name of ends) grip.addEventListener(name, onEnd); -+ const weekdays = (0, import_react38.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); -+ const month = (start, side) => { -+ const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); -+ const lead = (new Date(start).getUTCDay() + 6) % 7; -+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react38.createElement)("span", { key: `b${at}` })); -+ for (let day = 1; day <= length; day += 1) { -+ const ms = start + (day - 1) * DAY_MS; -+ const key = dayKey(ms); -+ const weekday = (lead + day - 1) % 7; -+ const flag = (on) => on ? "" : void 0; -+ cells.push((0, import_react38.createElement)("button", { -+ key, -+ type: "button", -+ className: "bt-us-day", -+ disabled: ms > todayMs, -+ "aria-label": dates({ year: "numeric", month: "long", day: "numeric", weekday: "short" }).format(ms), -+ "aria-pressed": key === low || key === high, -+ "data-in": flag(low !== null && key >= low && key <= high), -+ "data-start": flag(key === low), -+ "data-end": flag(key === high), -+ "data-l": flag(weekday === 0 || day === 1), -+ "data-r": flag(weekday === 6 || day === length), -+ "data-today": flag(key === today), -+ "data-has": flag(active.has(key)), -+ onMouseEnter: () => setHover(key), -+ onClick: () => press(key) -+ }, (0, import_react38.createElement)("b", null, day))); -+ } -+ const nav = side === "left" ? (0, import_react38.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react38.createElement)(ChevronLeftIcon)) : (0, import_react38.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react38.createElement)(ChevronRightIcon)); -+ return (0, import_react38.createElement)( -+ "div", -+ { key: start, className: "bt-us-month" }, -+ (0, import_react38.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react38.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), -+ (0, import_react38.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react38.createElement)("span", { key: at }, name))), -+ (0, import_react38.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) -+ ); - }; -- const hasStrip = canBrowse && tabState.ids.length > 0; -- return (0, import_react44.createElement)( -- "aside", -- { -- ref: root, -- className: "bt-panel", -- "data-view": view, -- "aria-label": view === "browser" && bot ? t("{name}'s browser", { name: bot.name }) : t("Conversation details"), -- "data-leaving": leaving || void 0, -- "aria-hidden": leaving || void 0, -- "data-settling": settling || void 0, -- "data-instant": instant || void 0, -- "data-dragging": dragging || void 0, -- style: { "--bt-panel-w": `${panelWidth}px` }, -- onAnimationEnd: (event) => { -- if (event.target === event.currentTarget) setLanded(true); -- } -- }, -- leaving ? null : (0, import_react44.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), -- // Over the whole window while dragging: the cursor stays a resize cursor and the -- // pages underneath (the chat, the guest) see no pointer. -- dragging ? (0, import_react_dom10.createPortal)((0, import_react44.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, -- hasStrip ? (0, import_react44.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, -- view === "browser" && bot ? (0, import_react44.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react44.createElement)( -+ const shortcuts = [ -+ ["month", t("This month"), dayKey(thisMonth), today], -+ ["last", t("Last month"), dayKey(addMonths(thisMonth, -1)), dayKey(thisMonth - DAY_MS)], -+ ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] -+ ]; -+ const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; -+ return (0, import_react38.createElement)( -+ "div", -+ { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, -+ (0, import_react38.createElement)( - "div", -- { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, -- (0, import_react44.createElement)(DetailsDrawer, { -- sessionId: pane.id, -- startRenaming: pane.renaming, -- startTab: pane.tab, -- hasStrip, -- roster, -- actions: panelActions, -- useSessions, -- useSessionStatus, -- useActivity, -- useWork, -- useBrowserTabs, -- useBrowserPages -- }) -+ { className: "bt-us-pick-quick" }, -+ shortcuts.map(([id, label, from, to]) => (0, import_react38.createElement)("button", { -+ key: id, -+ type: "button", -+ "aria-pressed": span?.from === from && span?.to === to, -+ onClick: () => onPick(from, to) -+ }, label)) -+ ), -+ (0, import_react38.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-pick-foot", role: "status" }, -+ (0, import_react38.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), -+ low === null ? null : (0, import_react38.createElement)("span", null, (0, import_react38.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) - ) - ); - } --var BROWSER_CSS = ` --.bt-panel{--bt-panel-view-fade:${SPRING_SHAPE.duration}ms} --.bt-ptabs{position:relative;display:flex;align-items:center;height:44px;flex:none;border-bottom:.5px solid var(--bt-line);padding:0 6px;gap:0;animation:bt-strip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --@keyframes bt-strip-in{from{height:0;opacity:0}} --.bt-ptabs-in{position:relative;display:flex;align-items:center;gap:2px;flex:1;min-width:0;height:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;padding:4px 2px;scroll-padding-left:48px;mask-image:linear-gradient(to right,#000,#000 calc(100% - 10px),transparent)} --.bt-ptabs-in>.bt-ptab-bot{position:sticky;left:0;z-index:2;background:var(--bt-main)} --.bt-ptabs-in>.bt-ptab-bot[aria-selected=true]{background:transparent} --.bt-ptabs-in::-webkit-scrollbar{display:none} --.bt-ptab{position:relative;z-index:1;display:inline-flex;align-items:center;gap:6px;height:32px;max-width:180px;padding:0 8px;flex:none;border:0;border-radius:9px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12.5px;cursor:pointer;white-space:nowrap;transition:color .12s ease} --.bt-ptab:hover{color:var(--bt-ink)} --.bt-ptab[aria-selected=true]{color:var(--bt-ink)} --.bt-ptab-bot{padding:0 7px} --.bt-ptab-bot .bt-mark{flex:none} --.bt-ptab-name{max-width:96px;overflow:hidden;text-overflow:ellipsis;font-weight:500;animation:bt-ptab-label .18s ease} --@keyframes bt-ptab-label{from{opacity:0;filter:blur(4px);max-width:0}} --.bt-ptab-fav{width:14px;height:14px;flex:none;border-radius:3px;object-fit:cover;color:var(--bt-ink-3)} --.bt-ptab-globe{display:inline-flex} --.bt-ptab-globe svg{width:14px;height:14px} --.bt-ptab-title{max-width:140px;overflow:hidden;text-overflow:ellipsis} --.bt-ptab-x{flex:none;width:16px;height:16px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;color:var(--bt-ink-3);opacity:0;transition:opacity .12s ease,background-color .12s ease} --.bt-ptab:hover .bt-ptab-x,.bt-ptab[aria-selected=true] .bt-ptab-x{opacity:1} --.bt-ptab-x:hover{background:var(--bt-active);color:var(--bt-ink)} --.bt-ptab-new{position:relative;z-index:1;flex:none;width:28px;height:32px;border:0;border-radius:9px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer} --.bt-ptab-new:hover{color:var(--bt-ink)} --.bt-ptab-new svg{width:15px;height:15px} --.bt-ptab-ind{position:absolute;top:4px;bottom:4px;border-radius:9px;background:var(--bt-hover);opacity:0;pointer-events:none;z-index:0;transition:opacity .15s ease} --.bt-ptab-close{flex:none;margin-left:2px} --.bt-panel-ghost{position:absolute;top:44px;right:0;bottom:0;overflow:hidden;pointer-events:none;z-index:2} --.bt-panel-ghost>.bt-panel-view{position:absolute;inset:0} --.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column;animation:bt-panel-view-in .22s cubic-bezier(.16,1,.3,1)} --@keyframes bt-panel-view-in{from{opacity:0}} --/* While the panel changes width its views keep their final width on the right edge, so -- only the panel edge moves and nothing inside reflows frame by frame. */ --.bt-panel[data-settling]{overflow:hidden} --.bt-panel[data-settling]>.bt-ptabs,.bt-panel[data-settling]>.bt-panel-view{flex-shrink:0;align-self:flex-end;box-sizing:border-box;width:calc(var(--bt-panel-w,320px) - .5px)} --.bt-panel-browser{position:relative} --.bt-browser-bar{position:relative;display:flex;align-items:center;gap:2px;height:52px;padding:0 10px 0 12px;flex:none;border-bottom:.5px solid var(--bt-line)} --.bt-browser-bar .bt-icon-btn:disabled{opacity:.35;cursor:default;background:none} --.bt-browser-address{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:32px;margin:0 4px;padding:0 12px;border-radius:999px;background:var(--bt-hover);border:.5px solid transparent;transition:background-color .12s ease,border-color .12s ease,box-shadow .12s ease} --.bt-browser-address:hover{background:var(--bt-active)} --.bt-browser-address[data-editing]{background:var(--bt-main);border-color:var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04)} --.bt-browser-scheme{display:inline-flex;color:var(--bt-ink-3);flex:none} --.bt-browser-scheme svg{width:14px;height:14px} --.bt-browser-address input{flex:1;min-width:0;border:0;background:none;outline:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;padding:0;text-overflow:ellipsis} --.bt-browser-address input::placeholder{color:var(--bt-ink-3)} --.bt-browser-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;pointer-events:none} --.bt-browser-progress::after{content:"";position:absolute;inset:0;width:40%;background:var(--bt-accent,#4D6BFE);border-radius:2px;animation:bt-browser-load 1.1s cubic-bezier(.4,0,.2,1) infinite} --@keyframes bt-browser-load{from{transform:translateX(-100%)}to{transform:translateX(250%)}} --.bt-browser-frame{position:relative;flex:1;min-height:0;background:var(--bt-main)} --.bt-browser-frame[data-guest]{background:#fff} --.bt-browser-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:32px;text-align:center;animation:bt-foot-in .3s ease} --.bt-browser-empty-title{font-size:15px;line-height:22px;font-weight:500;margin-top:6px} --.bt-browser-empty-text{max-width:300px;font-size:13px;line-height:19px;color:var(--bt-ink-2);overflow-wrap:anywhere} --.bt-browser-retry{margin-top:6px;height:32px;padding:0 14px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer} --.bt-browser-retry:hover{background:var(--bt-hover)} --.bt-bcards{display:flex;flex-direction:column;gap:12px} --.bt-bcard{position:relative;border:.5px solid var(--bt-line-2);border-radius:12px;overflow:hidden;background:var(--bt-main);animation:bt-bcard-in .24s cubic-bezier(.16,1,.3,1) both;transition:border-color .12s ease,box-shadow .12s ease} --@keyframes bt-bcard-in{from{opacity:0;filter:blur(4px);transform:translateY(4px)}} --.bt-bcard[data-active]{border-color:var(--bt-accent,#4D6BFE);box-shadow:0 0 0 1px var(--bt-accent,#4D6BFE)} --.bt-bcard-shot{position:relative;display:block;width:100%;aspect-ratio:16/10;border:0;background:var(--bt-hover);padding:0;cursor:pointer} --.bt-bcard-blank{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--bt-ink-3);font-size:12px} --.bt-bcard-blank svg{width:20px;height:20px} --.bt-bcard-preview{position:absolute;inset:0;display:flex;flex-direction:column;gap:6px;padding:14px 16px;text-align:left;background:linear-gradient(to bottom,var(--bt-main),var(--bt-hover));overflow:hidden} --.bt-bcard-ptitle{font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} --.bt-bcard-excerpt{font-size:12px;line-height:17px;color:var(--bt-ink-2);display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} --.bt-bcard-row{display:flex;align-items:center;gap:7px;padding:8px 10px;min-width:0} --.bt-bcard-fav{width:14px;height:14px;flex:none;border-radius:3px;color:var(--bt-ink-3)} --.bt-bcard-row svg.bt-bcard-fav{width:14px;height:14px} --.bt-bcard-title{flex:1;min-width:0;font-size:12.5px;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink)} --.bt-bcard-host{flex:none;font-size:11px;line-height:15px;color:var(--bt-ink-3);max-width:38%;overflow:hidden;text-overflow:ellipsis} --.bt-bcard-x{position:absolute;top:6px;right:6px;width:22px;height:22px;border:0;border-radius:7px;background:color-mix(in srgb,var(--bt-main) 88%,transparent);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;font-size:14px;cursor:pointer;opacity:0;transition:opacity .12s ease;backdrop-filter:blur(4px)} --.bt-bcard:hover .bt-bcard-x{opacity:1} --.bt-bcard-new{display:flex;align-items:center;justify-content:center;gap:6px;height:40px;border:.5px dashed var(--bt-line-2);border-radius:12px;background:none;color:var(--bt-ink-3);font:inherit;font-size:12.5px;cursor:pointer} --.bt-bcard-new:hover{color:var(--bt-ink);border-color:var(--bt-ink-3)} --.bt-bcard-new svg{width:14px;height:14px} --.bt-browser-empty-mark{display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center} --@media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} --`; -- --// src/client/remote-bot-settings.js --var import_react45 = require("react"); --var REMOTE_BOT_SETTINGS_CSS = ` --.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} --.bt-remote-sheet-layer[data-leaving]{pointer-events:none} --.bt-remote-sheet-layer .bt-scrim{position:absolute;inset:0;z-index:0;pointer-events:auto} --.bt-remote-sheet-layer[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} --.bt-remote-sheet:focus{outline:none} --.bt-remote-sheet{position:relative;z-index:1;pointer-events:auto;width:min(320px,calc(100vw - 32px));max-height:min(70vh,420px);margin:0 auto 0;border-radius:14px;background:var(--bt-card,#fff);border:.5px solid var(--bt-line-2);box-shadow:0 12px 40px rgba(20,20,20,.16);display:flex;flex-direction:column;overflow:hidden;animation:bt-remote-sheet-in .2s ease-out} --.bt-remote-sheet-layer[data-leaving] .bt-remote-sheet{animation:bt-remote-sheet-out .16s ease-in forwards} --.bt-remote-sheet-head{display:flex;align-items:center;gap:10px;padding:12px 12px 8px} --.bt-remote-sheet-who{display:flex;flex-direction:column;min-width:0;gap:1px;flex:1 1 auto} --.bt-remote-sheet-name{font-size:16px;line-height:22px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-remote-sheet-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-remote-sheet-body{padding:0 12px 14px;overflow:auto} --@keyframes bt-remote-sheet-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} --@keyframes bt-remote-sheet-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(8px)}} --@media (min-width:520px){ --.bt-remote-sheet-layer{align-items:center;padding:24px} --.bt-remote-sheet{margin:0} --} --`; --function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { -- const close = () => actions.closeSettings(); -- useEscape(close); -- const dialog = (0, import_react45.useRef)(null); -- (0, import_react45.useEffect)(() => { -- const before = document.activeElement; -- dialog.current?.focus?.({ preventScroll: true }); -- return () => { -- if (before && typeof before.focus === "function" && before.isConnected) before.focus({ preventScroll: true }); -- }; -- }, []); -- const onKeyDown = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -+function Heat({ view, keep, paint, index }) { -+ const [hover, setHover] = (0, import_react38.useState)(null); -+ const today = hourMs(`${view.now.slice(0, 10)} 00`); -+ const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; -+ const from = dayKey(first); -+ const daily = /* @__PURE__ */ new Map(); -+ for (const row of view.rows) { -+ const day = row[HOUR].slice(0, 10); -+ if (day >= from && keep(row)) daily.set(day, (daily.get(day) ?? 0) + tokensOf(row)); -+ } -+ const most = Math.max(0, ...daily.values()); -+ const levels = [0.3, 0.52, 0.76, 1]; -+ const months = []; -+ const cells = []; -+ for (let week = 0; week < HEAT_WEEKS; week += 1) { -+ const monday = first + week * 7 * DAY_MS; -+ if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { -+ months.push((0, import_react38.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); - } -- }; -- if (!bot?.remote) return null; -- const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); -- return (0, import_react45.createElement)( -- "div", -- { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, -- (0, import_react45.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -- (0, import_react45.createElement)( -+ for (let day = 0; day < 7; day += 1) { -+ const ms = monday + day * DAY_MS; -+ const value = daily.get(dayKey(ms)) ?? 0; -+ const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; -+ cells.push((0, import_react38.createElement)("span", { -+ key: ms, -+ className: "bt-us-cell", -+ style: { "--w": week }, -+ "data-on": hover?.ms === ms ? "" : void 0, -+ "data-future": ms > today ? "" : void 0, -+ "data-today": ms === today ? "" : void 0, -+ onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) -+ }, level > 0 ? (0, import_react38.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); -+ } -+ } -+ const tipSide = hover ? { -+ ...hover.week >= HEAT_WEEKS / 2 ? { right: `calc(${(HEAT_WEEKS - hover.week) / HEAT_WEEKS * 100}% + 8px)` } : { left: `calc(${(hover.week + 1) / HEAT_WEEKS * 100}% + 8px)` }, -+ ...hover.day < 4 ? { top: `calc(${hover.day / 7 * 100}% - 2px)` } : { top: "auto", bottom: `calc(${(6 - hover.day) / 7 * 100}% - 2px)` } -+ } : null; -+ const active = [...daily.values()].filter((value) => value > 0).length; -+ const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); -+ return (0, import_react38.createElement)( -+ "section", -+ { className: "bt-us-card bt-us-heat", style: { "--i": index } }, -+ (0, import_react38.createElement)("div", { className: "bt-us-card-head" }, (0, import_react38.createElement)("h3", null, t("Activity")), (0, import_react38.createElement)("span", { className: "bt-us-head-note" }, summary)), -+ (0, import_react38.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), -+ (0, import_react38.createElement)( - "div", -- { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, -- (0, import_react45.createElement)( -+ { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, -+ (0, import_react38.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), -+ hover ? (0, import_react38.createElement)( - "div", -- { className: "bt-remote-sheet-head" }, -- (0, import_react45.createElement)(BotMark, { bot, size: 40, live: true }), -- (0, import_react45.createElement)( -- "div", -- { className: "bt-remote-sheet-who" }, -- (0, import_react45.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), -- (0, import_react45.createElement)("span", { className: "bt-remote-sheet-sub" }, who) -- ), -- (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react45.createElement)(CloseIcon)) -- ), -- (0, import_react45.createElement)( -+ { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, -+ (0, import_react38.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), -+ (0, import_react38.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) -+ ) : null -+ ), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-heat-scale", "aria-hidden": true }, -+ t("Light"), -+ levels.map((level) => (0, import_react38.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react38.createElement)("i", { style: { background: soften(paint, level) } }))), -+ t("Heavy") -+ ) -+ ); -+} -+function Skeleton2() { -+ return (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us", "aria-busy": true }, -+ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-sheet bt-us-skel-sheet" }, -+ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), -+ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), -+ (0, import_react38.createElement)( - "div", -- { className: "bt-remote-sheet-body" }, -- bot.remote.handle ? (0, import_react45.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, -- (0, import_react45.createElement)(FlyBotDetails, { bot, roster, actions }) -+ { className: "bt-us-pair" }, -+ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), -+ (0, import_react38.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) - ) - ) - ); - } -- --// src/client/overlays.js --var import_react46 = require("react"); --var import_react_dom11 = require("react-dom"); --var DIALOG_EXIT_MS = 160; --var MENU_EXIT_MS3 = 120; --function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -- const roster = useRoster((value) => value); -- const ui2 = useUi((value) => value); -- useRegistry((value) => value); -- const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); -- const previous = (0, import_react46.useRef)(current); -- (0, import_react46.useEffect)(() => { -- if (previous.current === current) return; -- previous.current = current; -- if (current) actions.followPanel?.(current); -- }, [current]); -- const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; -- const [kept, setKept] = (0, import_react46.useState)(open); -- (0, import_react46.useEffect)(() => { -- if (open) { -- setKept(open); -- return void 0; -+var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null; -+function UsagePage({ roster, actions, focusId }) { -+ const [view, setView] = (0, import_react38.useState)(null); -+ const [error, setError] = (0, import_react38.useState)(""); -+ const [rangeId, setRangeId] = (0, import_react38.useState)("7d"); -+ const [span, setSpan] = (0, import_react38.useState)(null); -+ const [picking, setPicking] = (0, import_react38.useState)(false); -+ const [by, setBy] = (0, import_react38.useState)("bot"); -+ const tabs = (0, import_react38.useRef)(null); -+ const focus = focusOf(focusId); -+ (0, import_react38.useEffect)(() => { -+ let stopped = false; -+ let timer; -+ const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -+ }))); -+ const load = async () => { -+ try { -+ const value = await actions.usage(); -+ await resized; -+ if (stopped) return; -+ (0, import_react38.startTransition)(() => setView(value)); -+ setError(""); -+ timer = setTimeout(load, value.scanning ? 3e3 : 2e4); -+ } catch (failure) { -+ if (stopped) return; -+ setError(failure?.message ?? String(failure)); -+ timer = setTimeout(load, 1e4); -+ } -+ }; -+ void load(); -+ return () => { -+ stopped = true; -+ clearTimeout(timer); -+ }; -+ }, []); -+ const today = view?.now.slice(0, 10); -+ const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); -+ const shaped = (0, import_react38.useMemo)(() => { -+ if (!view) return null; -+ const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); -+ const lifetime = /* @__PURE__ */ new Map(); -+ for (const row of view.rows) lifetime.set(row[MODEL], (lifetime.get(row[MODEL]) ?? 0) + tokensOf(row)); -+ const modelPaint = new Map([...lifetime].sort((a, b) => b[1] - a[1]).map(([model], rank) => [model, `var(--bt-ink-${MODEL_INKS[rank % MODEL_INKS.length]})`])); -+ const models2 = view.models.map((model, at) => { -+ const name = model.name || model.model || t("Unknown model"); -+ const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; -+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react38.createElement)(ModelGlyph, { name, paint, size }) }; -+ }); -+ const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; -+ const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -+ const isBot2 = (at) => ["bot", "agent"].includes(view.owners[at]?.kind); -+ const all2 = summarize(view, range, void 0, (row) => by === "bot" ? isBot2(row[OWNER]) ? `o${row[OWNER]}` : "rest" : `m${row[MODEL]}`); -+ const keep2 = ownerAt2 >= 0 ? (row) => row[OWNER] === ownerAt2 : modelAt >= 0 ? (row) => row[MODEL] === modelAt : () => true; -+ const face2 = ownerAt2 >= 0 ? faces2[ownerAt2] : modelAt >= 0 ? models2[modelAt] : null; -+ const part2 = face2 ? summarize(view, range, keep2) : all2; -+ const activeDays2 = /* @__PURE__ */ new Set(); -+ let first = view.now; -+ for (const row of view.rows) { -+ if (row[HOUR] < first) first = row[HOUR]; -+ if (keep2(row) && tokensOf(row) > 0) activeDays2.add(row[HOUR].slice(0, 10)); - } -- const timer = setTimeout(() => setKept(null), PANEL_LINGER_MS); -- return () => clearTimeout(timer); -- }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); -- const pane = open ?? kept; -- const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); -- const remoteBotId = ui2.remoteBotSettings && roster.byId[ui2.remoteBotSettings]?.remote ? ui2.remoteBotSettings : null; -- const [remoteBotSettings, remoteLeaving] = useLinger(remoteBotId, DIALOG_EXIT_MS); -- const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS); -- const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); -- const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); -- const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); -- return (0, import_react46.createElement)( -- import_react46.Fragment, -- null, -- pane ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, -- ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, -- ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, -- ui2.exchange ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, -- ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, -- menu ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, -- settings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, -- remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, -- memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, -- inbox ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, -- board ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null -- ); --} -- --// src/client/read-page.js --var READ_LIMITS = { -- chars: 4e5, -- nodes: 15e4, -- depth: 400, -- ms: 1500, -- items: 3e3, -- tableRows: 5e3, -- tableCols: 30, -- frameDepth: 3 --}; --function readPage(limits) { -- const started = performance.now(); -- const SKIP = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link", "svg", "object", "embed", "audio", "video", "source", "track", "map", "datalist", "option", "optgroup"]); -- const ITEM = 'a[href], button, input:not([type=hidden]), textarea, select, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=checkbox], [role=switch], [role=radio], [contenteditable=""], [contenteditable=true]'; -- const ROLES = { a: "link", button: "button", select: "select", textarea: "textbox", summary: "button" }; -- const INPUT_ROLES = { checkbox: "checkbox", radio: "radio", submit: "button", button: "button", reset: "button", image: "button", search: "searchbox", range: "slider", file: "button", color: "button" }; -- const NO_VALUE_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio", "submit", "button", "reset", "image", "file", "color"]); -- const SECRET_HINT = /(^|[-_\s])(cvv|cvc|csc|cc-?(num|number|csc|exp)|card-?(num|number)|otp|one-?time-?code|pin)([-_\s]|$)/i; -- const BLOCK_DISPLAY = /^(block|flex|grid|list-item|table|flow-root|table-caption|table-row-group|table-header-group|table-footer-group|table-row|-webkit-box)$/; -- const INLINE_BLOCK = /^(inline-block|inline-flex|inline-grid|inline-table|table-cell|-webkit-inline-box)$/; -- const SEMANTIC = /^(ul|ol|table|pre|blockquote|h[1-6]|code|details)$/; -- const items = []; -- const stash = []; -- const stats = { nodes: 0, frames: 0 }; -- let chars = 0; -- let stop = ""; -- const collapse = (value) => String(value || "").replace(/\s+/g, " ").trim(); -- const isSecret = (el) => { -- if (el.localName !== "input" && el.localName !== "textarea") return false; -- const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); -- return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); -- }; -- const keep = (text3, end = "") => { -- if (text3 === "") return ""; -- stash.push(text3); -- return ` -- --${stash.length - 1}${end} -- --`; -- }; -- const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); -- const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); -- const block = (text3) => expand(flow(text3)); -- const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); -- const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); -- const childrenOf = (el) => { -- if (el.shadowRoot) return el.shadowRoot.childNodes; -- if (el.localName === "slot" && typeof el.assignedNodes === "function") { -- const assigned = el.assignedNodes(); -- if (assigned.length > 0) return assigned; -+ const oldest2 = Math.min(monthOf(hourMs(first)), addMonths(monthOf(hourMs(view.now)), -12)); -+ const ranked2 = (map) => [...map].filter(([, sum]) => sum.total > 0).sort((a, b) => b[1].total - a[1].total); -+ let series2; -+ if (face2) { -+ series2 = [{ key: "all", name: face2.name, fill: soften(face2.paint) }]; -+ } else { -+ if (by === "bot") { -+ const bots = ranked2(all2.owners).filter(([at]) => isBot2(at)); -+ series2 = bots.slice(0, TOP_SERIES).map(([at]) => ({ key: `o${at}`, name: faces2[at].name, paint: faces2[at].paint, pick: { kind: "owner", key: view.owners[at].key } })); -+ if (bots.length > TOP_SERIES) series2.push({ key: "more", name: t("More Bots"), paint: "var(--bt-us-more)" }); -+ if (ranked2(all2.owners).some(([at]) => !isBot2(at))) series2.push({ key: "rest", name: t("Other"), paint: "var(--bt-us-rest)" }); -+ } else { -+ const used = ranked2(all2.models); -+ series2 = used.slice(0, TOP_SERIES).map(([at]) => ({ key: `m${at}`, name: models2[at].name, paint: models2[at].paint, pick: { kind: "model", key: view.models[at].key } })); -+ if (used.length > TOP_SERIES) series2.push({ key: "more", name: t("More models"), paint: "var(--bt-us-more)" }); -+ } -+ const repeats = /* @__PURE__ */ new Map(); -+ for (const entry of series2) { -+ const seen = repeats.get(entry.paint) ?? 0; -+ repeats.set(entry.paint, seen + 1); -+ entry.fill = soften(entry.paint, [1, 0.6, 0.36][seen % 3]); -+ } - } -- return el.childNodes; -- }; -- const over = () => { -- if (stop) return true; -- if (chars > limits.chars) stop = `the text passed ${limits.chars} characters`; -- else if (stats.nodes > limits.nodes) stop = `the page has more than ${limits.nodes} nodes`; -- else if (stats.nodes % 512 === 0 && performance.now() - started > limits.ms) stop = `reading took longer than ${limits.ms} ms`; -- return stop !== ""; -- }; -- const roleOf = (el) => el.getAttribute("role") || ROLES[el.localName] || (el.localName === "input" ? INPUT_ROLES[el.type] || "textbox" : el.isContentEditable ? "textbox" : el.localName); -- const sized = (el) => { -- const box = el.getBoundingClientRect(); -- return box.width > 0 && box.height > 0; -- }; -- const itemFor = (el, style) => { -- if (items.length >= limits.items || style.visibility !== "visible" || !el.matches(ITEM) || !sized(el)) return null; -- const item = { role: roleOf(el) }; -- const secret = isSecret(el); -- if (el.href) item.href = String(el.href); -- if ((el.localName === "input" || el.localName === "textarea") && !secret && !NO_VALUE_TYPES.has(el.type) && el.value) item.value = String(el.value).slice(0, 120); -- if (el.localName === "select" && el.selectedOptions?.length) item.value = collapse([...el.selectedOptions].map((option) => option.label || option.text).join(", ")).slice(0, 120); -- if (el.checked) item.checked = true; -- if (el.disabled) item.disabled = true; -- items.push(item); -- return { item, number: items.length, secret }; -- }; -- const nameOf = (el, text3, secret) => collapse( -- el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" -- ).slice(0, 80); -- const fenced = (code, lang) => { -- const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); -- const fence = "`".repeat(longest + 1); -- return `${fence}${lang} --${code.replace(/\n+$/, "")} --${fence}`; -- }; -- const langOf = (el) => { -- const names = `${el.className || ""} ${el.querySelector?.("code")?.className || ""}`; -- return /(?:^|\s)(?:lang|language)-([\w+#.-]+)/.exec(names)?.[1] || ""; -- }; -- const list = (el, ctx) => { -- const ordered = el.localName === "ol"; -- let number = ordered ? Number(el.getAttribute("start") || 1) || 1 : 1; -- const lines = []; -- for (const child of childrenOf(el)) { -- if (over()) break; -- if (child.nodeType !== 1 || child.localName !== "li") { -- const loose = block(render(child, ctx)); -- if (loose) lines.push(loose); -- continue; -- } -- const style = styleOf(child); -- if (style.display === "none") continue; -- let body2 = block(children(child, { ...ctx, depth: ctx.depth + 1, shown: style.visibility === "visible" })); -- if (!body2.includes("```")) body2 = body2.replace(/\n{2,}/g, "\n"); -- const marker = ordered ? `${number++}. ` : "- "; -- if (body2 === "") continue; -- const pad2 = " ".repeat(marker.length); -- lines.push(body2.split("\n").map((line2, index) => (index === 0 ? marker : line2 ? pad2 : "") + line2).join("\n")); -- } -- return keep(lines.join("\n")); -- }; -- const cellText = (cell, ctx) => block(children(cell, ctx)).replace(/\n+/g, "
").replace(/\|/g, "\\|"); -- const layoutTable = (table, rows) => { -- const role = table.getAttribute("role"); -- if (role === "presentation" || role === "none") return true; -- if (table.querySelector("table")) return true; -- return !rows.some((row) => row.cells.length > 1); -- }; -- const tableMarkdown = (table, ctx) => { -- const rows = [...table.rows].filter((row) => styleOf(row).display !== "none"); -- const caption = table.caption ? oneLine(children(table.caption, ctx)) : ""; -- if (layoutTable(table, rows)) { -- return rows.map((row) => [...row.cells].map((cell) => ` -- --${children(cell, ctx)} -- --`).join("")).join(""); -- } -- const grid = []; -- const carry = []; -- let width = 0; -- for (const row of rows.slice(0, limits.tableRows + 1)) { -- if (over()) break; -- const line2 = []; -- let column = 0; -- const place = () => { -- while (carry[column]?.left > 0) { -- line2[column] = carry[column].text; -- carry[column].left -= 1; -- column += 1; -- } -- }; -- for (const cell of row.cells) { -- place(); -- if (styleOf(cell).display === "none") continue; -- const text3 = cellText(cell, ctx); -- const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); -- const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); -- for (let offset = 0; offset < span; offset += 1) { -- line2[column] = offset === 0 ? text3 : ""; -- if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; -- column += 1; -- } -- } -- place(); -- width = Math.max(width, line2.length); -- grid.push(line2); -+ const tipSeries2 = face2 ? PARTS.map(([key, strength]) => ({ key, name: partLabel(key), fill: soften(face2.paint, strength) })) : series2; -+ const fillOf2 = new Map(series2.map((entry) => [entry.key, entry.fill])); -+ const shownKeys = new Set(series2.map((entry) => entry.key)); -+ const columns2 = part2.buckets.map((bucket) => { -+ const parts = face2 ? [["all", bucket.sum.total]] : series2.map((entry) => [entry.key, entry.key === "more" ? [...bucket.stacks].filter(([key]) => key !== "rest" && !shownKeys.has(key)).reduce((sum, [, value]) => sum + value, 0) : bucket.stacks.get(entry.key) ?? 0]); -+ const tip = face2 ? PARTS.map(([key]) => [key, bucket.sum[key]]) : parts; -+ return { key: bucket.key, tick: tickOf(range, bucket), title: titleOf(range, bucket), total: bucket.sum.total, parts, tip }; -+ }); -+ return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; -+ }, [view, rangeId, span, by, focusId, roster]); -+ const root = (0, import_react38.useRef)(null); -+ (0, import_react38.useEffect)(() => { -+ root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); -+ }, [focusId]); -+ if (!view) { -+ return error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react38.createElement)(Skeleton2); -+ } -+ if (view.rows.length === 0) { -+ return (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us" }, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-empty" }, -+ (0, import_react38.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), -+ (0, import_react38.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), -+ (0, import_react38.createElement)("span", null, t("Token usage shows up here after a model is called.")) -+ ) -+ ); -+ } -+ const { faces, models, ownerAt, face, keep, all, part, series, tipSeries, fillOf, columns, ranked, isBot, activeDays, oldest } = shaped; -+ const pick2 = (next) => actions.openSettings("usage", `${next.kind}:${next.key}`); -+ const chooseRange = (id) => { -+ if (id !== "custom") { -+ setRangeId(id); -+ setPicking(false); -+ return; - } -- width = Math.min(width, limits.tableCols); -- if (grid.length === 0 || width === 0) return caption ? ` -- --${caption} -- --` : ""; -- const format = (cells) => `| ${Array.from({ length: width }, (_, index) => cells[index] ?? "").join(" | ")} |`; -- const out = [format(grid[0]), `|${" --- |".repeat(width)}`, ...grid.slice(1).map(format)]; -- if (rows.length > limits.tableRows + 1) out.push(`… (table cut: ${rows.length - limits.tableRows - 1} more rows)`); -- return `${caption ? ` -- --Table: ${caption}` : ""}${keep(out.join("\n"))}`; -+ if (span) setRangeId("custom"); -+ setPicking((open) => !open); - }; -- const frame = (el, ctx) => { -- let doc = null; -- try { -- doc = el.contentDocument; -- } catch { -- doc = null; -- } -- const address = (() => { -- try { -- return doc?.location?.href || el.src || el.getAttribute("src") || "about:blank"; -- } catch { -- return el.src || "about:blank"; -- } -- })(); -- if (!doc) return ` -- --[iframe (cross-origin, not readable): ${address}] -- --`; -- if (ctx.frames >= limits.frameDepth) return ` -- --[iframe (nested too deep, not read): ${address}] -- --`; -- stats.frames += 1; -- const title = collapse(el.title || doc.title); -- const body2 = doc.body ? block(children(doc.body, { ...ctx, frames: ctx.frames + 1, depth: ctx.depth + 1, shown: true, pre: false })) : ""; -- return keep(`[iframe: ${title ? `${title} (${address})` : address}] --${body2 || "(no text)"} --[iframe end]`); -+ const pickSpan = (from, to) => { -+ setSpan({ from, to }); -+ setRangeId("custom"); -+ setPicking(false); - }; -- function children(el, ctx) { -- let out = ""; -- for (const child of childrenOf(el)) { -- if (over()) break; -- out += render(child, ctx); -- } -- return out; -+ const rangeOptions = [ -+ ...RANGES.map((item) => [item.id, rangeLabel(item.id)]), -+ ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] -+ ]; -+ const whole = part.total.total; -+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react38.createElement)(Row3, { -+ key: `o${at}`, -+ kind: faces[at].kind, -+ glyph: faces[at].glyph(32), -+ name: faces[at].name, -+ sub: faces[at].sub, -+ sum, -+ whole: total, -+ index, -+ fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), -+ onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) -+ }); -+ const modelRow = ([at, sum], index) => (0, import_react38.createElement)(Row3, { -+ key: `m${at}`, -+ kind: "model", -+ glyph: models[at].glyph(32), -+ name: models[at].name, -+ sub: models[at].sub, -+ sum, -+ whole, -+ index, -+ fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), -+ onClick: () => pick2({ kind: "model", key: view.models[at].key }) -+ }); -+ const nothing = (0, import_react38.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); -+ let body; -+ if (face) { -+ const kinds = ranked(part.kinds); -+ const usedModels = ranked(part.models); -+ const owners = ranked(part.owners); -+ body = [ -+ (0, import_react38.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), -+ (0, import_react38.createElement)( -+ "div", -+ { key: "sheet", className: "bt-us-sheet" }, -+ (0, import_react38.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), -+ (0, import_react38.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), -+ (0, import_react38.createElement)( -+ "div", -+ { key: "pair", className: "bt-us-pair" }, -+ ownerAt >= 0 ? (0, import_react38.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react38.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), -+ (0, import_react38.createElement)( -+ ListCard, -+ { title: t("What for"), index: 3 }, -+ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react38.createElement)(Row3, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing -+ ) -+ ), -+ (0, import_react38.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) -+ ) -+ ]; -+ } else { -+ const owners = ranked(all.owners); -+ const bots = owners.filter(([at]) => isBot(at)); -+ const rest = owners.filter(([at]) => !isBot(at)); -+ const usedModels = ranked(all.models); -+ body = [ -+ (0, import_react38.createElement)( -+ "div", -+ { key: "sheet", className: "bt-us-sheet" }, -+ (0, import_react38.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), -+ (0, import_react38.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), -+ (0, import_react38.createElement)( -+ "div", -+ { key: "pair", className: "bt-us-pair" }, -+ (0, import_react38.createElement)( -+ ListCard, -+ { title: t("Bots"), count: bots.length, index: 2 }, -+ owners.length > 0 ? null : nothing, -+ bots.map((entry, index) => ownerRow(entry, index)), -+ bots.length > 0 && rest.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, -+ rest.map((entry, index) => ownerRow(entry, bots.length + index)) -+ ), -+ (0, import_react38.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) -+ ), -+ (0, import_react38.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) -+ ) -+ ]; - } -- function render(node, ctx) { -- stats.nodes += 1; -- if (node.nodeType === 3) { -- if (!ctx.shown) return ""; -- const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); -- const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); -- chars += text3.length; -- return text3; -- } -- if (node.nodeType !== 1) return ""; -- const el = node; -- const tag = el.localName; -- if (SKIP.has(tag) || ctx.depth > limits.depth || over()) return ""; -- const style = styleOf(el); -- const display = style.display; -- if (display === "none" || style.contentVisibility === "hidden") return ""; -- const shown = style.visibility === "visible"; -- const keepsSpace = String(style.whiteSpace || "").startsWith("pre") || style.whiteSpace === "break-spaces"; -- const next = { ...ctx, depth: ctx.depth + 1, shown }; -- if (display === "contents" && !SEMANTIC.test(tag) && !el.matches(ITEM)) return children(el, next); -- if (style.overflowX !== "visible" || style.overflowY !== "visible") { -- const box = el.getBoundingClientRect(); -- if (box.width === 0 || box.height === 0) return ""; -- } -- if (tag === "br") return "\n"; -- if (tag === "hr") return "\n\n---\n\n"; -- if (tag === "img") { -- const alt = collapse(el.getAttribute("alt")); -- return shown && alt ? `[image: ${alt}]` : ""; -+ return (0, import_react38.createElement)( -+ "div", -+ { ref: root, className: "bt-us", key: focusId ?? "all" }, -+ (0, import_react38.createElement)( -+ "div", -+ { className: "bt-us-top" }, -+ (0, import_react38.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), -+ view.scanning ? (0, import_react38.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, -+ picking ? (0, import_react38.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null -+ ), -+ error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ body -+ ); -+} -+var USAGE_CSS = ` -+.bt-us{--bt-us-ease:cubic-bezier(.22,1,.36,1);--bt-us-rest:color-mix(in srgb,var(--bt-ink) 30%,var(--bt-card));--bt-us-more:color-mix(in srgb,var(--bt-ink) 16%,var(--bt-card));display:flex;flex-direction:column;gap:14px;padding-bottom:4px} -+.bt-us-top{position:relative;z-index:4;display:flex;align-items:center;gap:14px;flex-wrap:wrap} -+.bt-us-tabs{position:relative;display:inline-flex;flex:none;padding:3px;border-radius:999px;background:var(--bt-hover)} -+.bt-us-tabs button{position:relative;z-index:1;display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 12px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;line-height:26px;white-space:nowrap;cursor:pointer;transition:color .15s ease} -+.bt-us-tabs button svg{flex:none;width:13px;height:13px;margin-left:-2px} -+.bt-us-pick{position:absolute;top:calc(100% + 8px);left:0;max-width:100%;box-sizing:border-box;padding:14px 16px 12px;border-radius:18px;border:.5px solid var(--bt-line-2);background:var(--bt-card);box-shadow:0 22px 50px -18px rgba(0,0,0,.32),0 2px 8px rgba(0,0,0,.05);transform-origin:24px 0;animation:bt-us-drop .2s var(--bt-us-ease)} -+.bt-us-pick-quick{display:flex;gap:6px;margin-bottom:12px} -+.bt-us-pick-quick button{height:26px;padding:0 11px;border:0;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease} -+.bt-us-pick-quick button:hover{background:var(--bt-active);color:var(--bt-ink)} -+.bt-us-pick-quick button[aria-pressed=true]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:var(--bt-accent)} -+.bt-us-pick-months{display:flex;flex-wrap:wrap;gap:8px 28px} -+.bt-us-month{width:252px;animation:bt-us-fade .22s ease} -+.bt-us-month-head{position:relative;display:flex;align-items:center;justify-content:center;height:28px;margin-bottom:4px;font-size:13px;line-height:18px;font-weight:600} -+.bt-us-month-nav{position:absolute;top:0;width:28px;height:28px;padding:0;border:0;border-radius:9px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease} -+.bt-us-month-nav[data-side=left]{left:0} -+.bt-us-month-nav[data-side=right]{right:0} -+.bt-us-month-nav:hover:not(:disabled){background:var(--bt-hover);color:var(--bt-ink)} -+.bt-us-month-nav:disabled{opacity:.3;cursor:default} -+.bt-us-weekdays,.bt-us-days{display:grid;grid-template-columns:repeat(7,36px)} -+.bt-us-weekdays span{height:24px;font-size:11px;line-height:24px;text-align:center;color:var(--bt-ink-3)} -+.bt-us-day{position:relative;height:36px;padding:0;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:12px;font-variant-numeric:tabular-nums;cursor:pointer} -+.bt-us-day::before{content:'';position:absolute;top:4px;bottom:4px;left:0;right:0;transition:background-color .12s ease} -+.bt-us-day>b{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;font-weight:400;transition:background-color .12s ease,color .12s ease} -+.bt-us-day:hover:not(:disabled)>b{background:var(--bt-hover)} -+.bt-us-day[data-today]>b{color:var(--bt-accent);font-weight:600} -+.bt-us-day[data-has]::after{content:'';position:absolute;z-index:2;left:50%;bottom:6px;width:3px;height:3px;margin-left:-1.5px;border-radius:50%;background:var(--bt-ink-3)} -+.bt-us-day[data-in]::before{background:color-mix(in srgb,var(--bt-accent) 13%,transparent)} -+.bt-us-day[data-in][data-l]::before{left:4px;border-radius:14px 0 0 14px} -+.bt-us-day[data-in][data-r]::before{right:4px;border-radius:0 14px 14px 0} -+.bt-us-day[data-in][data-l][data-r]::before{border-radius:14px} -+.bt-us-day[data-in][data-start]::before{left:50%;border-radius:0} -+.bt-us-day[data-in][data-end]::before{right:50%;border-radius:0} -+.bt-us-day[data-in][data-start][data-r]::before,.bt-us-day[data-in][data-end][data-l]::before,.bt-us-day[data-in][data-start][data-end]::before{display:none} -+.bt-us-day[data-start]>b,.bt-us-day[data-end]>b,.bt-us-day[data-start]:hover>b,.bt-us-day[data-end]:hover>b{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} -+.bt-us-day[data-start][data-has]::after,.bt-us-day[data-end][data-has]::after{background:color-mix(in srgb,var(--bt-accent-ink,#fff) 75%,transparent)} -+.bt-us-day:disabled{color:var(--bt-ink-3);opacity:.45;cursor:default} -+.bt-us-pick-foot{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:8px;padding-top:10px;border-top:1px solid var(--bt-line);font-size:12px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap} -+.bt-us-pick-foot b{color:var(--bt-ink);font-weight:500} -+.bt-us-tabs button:hover,.bt-us-tabs button[aria-pressed=true]{color:var(--bt-ink)} -+.bt-us-tabs:not([data-glide]) button[aria-pressed=true]{background:var(--bt-card)} -+.bt-us-tabs>.bt-thumb{top:3px;bottom:3px;border-radius:999px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08),0 0 0 .5px var(--bt-line-2)} -+.bt-us-note{display:inline-flex;align-items:center;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-3)} -+.bt-us-note::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--bt-accent);animation:bt-us-pulse 1.4s ease-in-out infinite} -+.bt-us-sheet{display:flex;flex-direction:column;gap:4px} -+.bt-us-sheet>.bt-us-stats,.bt-us-sheet>.bt-us-card,.bt-us-sheet>.bt-us-pair>.bt-us-card{border-radius:0} -+.bt-us-sheet>:first-child{border-radius:16px 16px 0 0} -+.bt-us-sheet>:last-child{border-radius:0 0 16px 16px} -+.bt-us-stats{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;border-radius:16px;border:1px solid var(--bt-line);background:var(--bt-card);animation:bt-us-in .45s var(--bt-us-ease) backwards} -+.bt-us-stat{display:flex;flex-direction:column;justify-content:flex-end;gap:6px;min-width:0;padding:16px 18px} -+.bt-us-stat+.bt-us-stat{border-left:1px solid var(--bt-line)} -+.bt-us-stat-label{overflow:hidden;font-size:12px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap;text-overflow:ellipsis} -+.bt-us-stat-value{display:flex;align-items:baseline;gap:6px;min-width:0;font-size:22px;line-height:28px;font-weight:600;letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap} -+.bt-us-stat-main .bt-us-stat-value{font-size:34px;line-height:40px;letter-spacing:-.025em} -+.bt-us-stat-value small{font-size:13px;font-weight:400;letter-spacing:0;color:var(--bt-ink-3)} -+.bt-us-delta{align-self:center;margin-left:2px;padding:0 8px;border-radius:999px;background:var(--bt-tag-bg);color:var(--bt-ink-2);font-size:11px;line-height:20px;font-weight:500;letter-spacing:0} -+.bt-us-card{display:flex;flex-direction:column;gap:12px;min-width:0;padding:14px 18px 16px;border-radius:16px;border:1px solid var(--bt-line);background:var(--bt-card);animation:bt-us-in .45s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 45ms)} -+.bt-us-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:32px} -+.bt-us-card-head h3{display:flex;align-items:baseline;gap:8px;min-width:0;margin:0;font-size:13px;line-height:18px;font-weight:600} -+.bt-us-card-head h3 span,.bt-us-head-note{font-size:12px;font-weight:400;color:var(--bt-ink-3)} -+.bt-us-chart-body{display:flex;flex-direction:column;gap:8px;padding-top:6px} -+.bt-us-plot{position:relative;height:200px;margin-left:40px} -+.bt-us-grid{position:absolute;left:0;right:0;border-top:1px dashed var(--bt-line-2);pointer-events:none} -+.bt-us-grid.bt-us-base{border-top:1px solid var(--bt-line-2)} -+.bt-us-grid span{position:absolute;right:calc(100% + 8px);top:-8px;font-size:10px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} -+.bt-us-cols{position:absolute;inset:0;display:flex;align-items:flex-end} -+.bt-us-col{position:relative;flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end;justify-content:center} -+.bt-us-stack{display:flex;flex-direction:column-reverse;gap:2px;width:56%;min-width:4px;max-width:28px;transform-origin:50% 100%;animation:bt-us-grow .6s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 14ms);transition:height .45s var(--bt-us-ease),opacity .18s ease} -+.bt-us-stack>i{flex:1 1 0;min-height:2px;border-radius:5px;transition:flex-grow .45s var(--bt-us-ease)} -+.bt-us-plot[data-hover] .bt-us-col:not([data-on]) .bt-us-stack{opacity:.3} -+.bt-us-stub{width:4px;height:4px;margin-bottom:-2px;border-radius:50%;background:var(--bt-line-2)} -+.bt-us-ticks{display:flex;margin-left:40px;font-size:11px;line-height:14px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums} -+.bt-us-ticks span{flex:1 1 0;min-width:0;text-align:center;white-space:nowrap} -+.bt-us-ticks span[data-now]{color:var(--bt-ink);font-weight:500} -+.bt-us-tip{position:absolute;top:6px;z-index:3;min-width:160px;max-width:240px;padding:10px 12px;border-radius:12px;border:.5px solid var(--bt-line-2);background:color-mix(in srgb,var(--bt-card) 90%,transparent);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);box-shadow:0 14px 34px -12px rgba(0,0,0,.28);font-size:12px;line-height:16px;pointer-events:none;animation:bt-us-pop .16s var(--bt-us-ease)} -+.bt-us-tip-title{color:var(--bt-ink-3)} -+.bt-us-tip-total{margin:2px 0 8px;font-size:15px;line-height:20px;font-weight:600;font-variant-numeric:tabular-nums} -+.bt-us-tip-total:last-child{margin-bottom:0} -+.bt-us-tip-row{display:flex;align-items:center;gap:8px;color:var(--bt-ink-2)} -+.bt-us-tip-row+.bt-us-tip-row{margin-top:5px} -+.bt-us-tip-row span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-us-tip-row b{font-weight:500;color:var(--bt-ink);font-variant-numeric:tabular-nums} -+.bt-us-legend{display:flex;flex-wrap:wrap;gap:6px} -+.bt-us-key{display:inline-flex;align-items:center;gap:6px;min-width:0;height:24px;padding:0 10px 0 8px;border:0;border-radius:999px;background:var(--bt-hover);color:var(--bt-ink-2);font:inherit;font-size:12px;line-height:24px} -+button.bt-us-key{cursor:pointer;transition:background-color .12s ease,color .12s ease} -+button.bt-us-key:hover{background:var(--bt-active);color:var(--bt-ink)} -+.bt-us-dot{flex:none;width:8px;height:8px;border-radius:50%} -+.bt-us-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:4px} -+.bt-us-rows{display:flex;flex-direction:column;margin:0 -8px} -+.bt-us-row{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:8px;border:0;border-radius:12px;background:none;color:var(--bt-ink);font:inherit;text-align:left;animation:bt-us-in .4s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 35ms + 90ms)} -+button.bt-us-row{cursor:pointer;transition:background-color .12s ease} -+button.bt-us-row:hover{background:var(--bt-hover)} -+.bt-us-row-glyph{flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px} -+.bt-us-row-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:7px} -+.bt-us-row-line{display:flex;align-items:baseline;gap:8px;min-width:0;font-size:13px;line-height:18px} -+.bt-us-row-name{flex:0 1 auto;min-width:0;overflow:hidden;font-weight:500;text-overflow:ellipsis;white-space:nowrap} -+.bt-us-row-sub{flex:0 1 auto;min-width:0;overflow:hidden;font-size:12px;color:var(--bt-ink-3);text-overflow:ellipsis;white-space:nowrap} -+.bt-us-track{display:block;height:4px;border-radius:999px;background:var(--bt-hover);overflow:hidden} -+.bt-us-track>span{display:block;height:100%;border-radius:inherit;transform-origin:0 50%;animation:bt-us-fill .7s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 35ms + 140ms);transition:width .45s var(--bt-us-ease)} -+.bt-us-row-num{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:1px;min-width:52px;font-variant-numeric:tabular-nums} -+.bt-us-row-num b{font-size:13px;line-height:18px;font-weight:600} -+.bt-us-row-num small{font-size:11px;line-height:14px;color:var(--bt-ink-3)} -+.bt-us-split{height:1px;margin:6px 8px;background:var(--bt-line)} -+.bt-us-none{padding:18px 8px;font-size:12px;color:var(--bt-ink-3);text-align:center} -+.bt-us-glyph{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2)} -+.bt-us-model{display:inline-flex;align-items:center;justify-content:center;border-radius:30%;font-weight:600;line-height:1} -+.bt-us-profile{display:flex;align-items:center;gap:20px;padding:6px 4px 2px;animation:bt-us-in .45s var(--bt-us-ease) backwards} -+.bt-us-ring{position:relative;flex:none;width:96px;height:96px} -+.bt-us-ring>svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)} -+.bt-us-ring>svg circle{fill:none;stroke-width:4} -+.bt-us-ring-track{stroke:var(--bt-line)} -+.bt-us-ring-arc{stroke-linecap:round;transition:stroke-dasharray .6s var(--bt-us-ease);animation:bt-us-ring 1s var(--bt-us-ease) .1s backwards} -+.bt-us-ring-face{position:absolute;inset:0;display:flex;align-items:center;justify-content:center} -+.bt-us-profile-copy{display:flex;flex-direction:column;gap:3px;min-width:0} -+.bt-us-profile-copy h3{margin:0;overflow:hidden;font-size:22px;line-height:28px;font-weight:600;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap} -+.bt-us-profile-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-us-profile-share{font-size:12px;line-height:16px;color:var(--bt-ink-3)} -+.bt-us-heat{gap:8px} -+.bt-us-heat-months,.bt-us-heat-grid{display:grid;grid-template-columns:repeat(${HEAT_WEEKS},1fr);gap:3px} -+.bt-us-heat-months{height:14px;font-size:10px;line-height:14px;color:var(--bt-ink-3)} -+.bt-us-heat-months span{white-space:nowrap} -+.bt-us-heat-plot{position:relative} -+.bt-us-heat-grid{grid-template-rows:repeat(7,auto);grid-auto-flow:column} -+.bt-us-heat-grid .bt-us-cell[data-on]{outline:1.5px solid var(--bt-ink-2);outline-offset:1px} -+.bt-us-heat-tip{min-width:0;white-space:nowrap} -+.bt-us-cell{position:relative;aspect-ratio:1;border-radius:3px;background:var(--bt-hover);overflow:hidden} -+.bt-us-heat-grid .bt-us-cell{animation:bt-us-cell .5s var(--bt-us-ease) backwards;animation-delay:calc(var(--w,0) * 7ms + 120ms)} -+.bt-us-cell>i{position:absolute;inset:0} -+.bt-us-cell[data-today]{outline:1.5px solid var(--bt-ink-3);outline-offset:1px} -+.bt-us-cell[data-future]{visibility:hidden} -+.bt-us-heat-scale{display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} -+.bt-us-heat-scale .bt-us-cell{width:10px} -+.bt-us-skel{border-radius:16px;background:linear-gradient(90deg,var(--bt-hover) 0%,var(--bt-active) 50%,var(--bt-hover) 100%);background-size:200% 100%;animation:bt-us-shine 1.4s ease-in-out infinite} -+.bt-us-skel-sheet>.bt-us-skel:first-child{border-radius:16px 16px 0 0} -+.bt-us-skel-sheet>.bt-us-skel:not(:first-child){border-radius:0} -+.bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:first-child{border-radius:0 0 0 16px} -+.bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:last-child{border-radius:0 0 16px 0} -+.bt-us-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:72px 16px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)} -+.bt-us-empty b{margin-top:6px;font-size:15px;font-weight:600;color:var(--bt-ink)} -+@keyframes bt-us-in{from{opacity:0;transform:translateY(6px)}} -+@keyframes bt-us-grow{from{transform:scaleY(0)}} -+@keyframes bt-us-fill{from{transform:scaleX(0)}} -+@keyframes bt-us-pop{from{opacity:0;transform:translateY(3px) scale(.97)}} -+@keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} -+@keyframes bt-us-fade{from{opacity:0}} -+@keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} -+@keyframes bt-us-ring{from{stroke-dasharray:0 ${Math.ceil(RING2)}}} -+@keyframes bt-us-pulse{50%{opacity:.25}} -+@keyframes bt-us-shine{from{background-position:200% 0}to{background-position:-200% 0}} -+@media (prefers-reduced-motion:reduce){.bt-us *,.bt-us *::before{animation:none!important;transition:none!important}} -+`; -+ -+// src/client/connectors.js -+var import_react39 = require("react"); -+var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; -+var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; -+var FEISHU_WEBHOOK_URL = "https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot"; -+var APPS = [ -+ ["gmail", "Gmail"], -+ ["googlecalendar", "Google Calendar"], -+ ["googledrive", "Google Drive"], -+ ["googledocs", "Google Docs"], -+ ["googlesheets", "Google Sheets"], -+ ["outlook", "Outlook"], -+ ["slack", "Slack"], -+ ["microsoft_teams", "Microsoft Teams"], -+ ["discord", "Discord"], -+ ["telegram", "Telegram"], -+ ["whatsapp", "WhatsApp"], -+ ["notion", "Notion"], -+ ["linear", "Linear"], -+ ["jira", "Jira"], -+ ["asana", "Asana"], -+ ["trello", "Trello"], -+ ["todoist", "Todoist"], -+ ["airtable", "Airtable"], -+ ["dropbox", "Dropbox"], -+ ["hubspot", "HubSpot"], -+ ["salesforce", "Salesforce"], -+ ["stripe", "Stripe"], -+ ["zoom", "Zoom"], -+ ["calendly", "Calendly"], -+ ["figma", "Figma"], -+ ["twitter", "X (Twitter)"], -+ ["youtube", "YouTube"] -+]; -+var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; -+var GitHubMark = () => (0, import_react39.createElement)( -+ "svg", -+ { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, -+ (0, import_react39.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) -+); -+var FeishuMark = () => (0, import_react39.createElement)( -+ "svg", -+ { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -+ (0, import_react39.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }), -+ (0, import_react39.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }), -+ (0, import_react39.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" }) -+); -+var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark }; -+var ABOUT = { -+ github: () => t("Repositories, issues, and pull requests"), -+ composio: () => t("Gmail, Slack, Calendar, Notion and many more apps, through one Composio key"), -+ feishu: () => t("Posts into one Feishu group through a custom bot webhook") -+}; -+function statusText2(connector) { -+ if (connector.state === "connecting") return t("Connecting…"); -+ if (connector.state === "error") return t("Could not connect: {error}", { error: connector.error ?? "" }); -+ if (connector.state === "ready" && connector.kind === "channel") { -+ return connector.mode === "app" ? t("Connected: app, over the long connection") : t("Connected: custom bot webhook"); -+ } -+ if (connector.state === "ready" && connector.apps?.length > 0) return t("Connected: {apps}", { apps: connector.apps.map(appName).join(", ") }); -+ if (connector.state === "ready") return connector.account ? t("Connected as {account}", { account: connector.account }) : t("Connected"); -+ return ABOUT[connector.id]?.() ?? ""; -+} -+function CredentialField({ credential, value, onChange, onEnter, autoFocus }) { -+ const label = t(credential.label); -+ if (credential.masked) return (0, import_react39.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus }); -+ return (0, import_react39.createElement)( -+ "label", -+ { className: "bt-q-field" }, -+ (0, import_react39.createElement)("input", { -+ className: "bt-input", -+ type: "text", -+ value, -+ "aria-label": label, -+ placeholder: label, -+ spellCheck: false, -+ autoFocus, -+ onChange: (event) => onChange(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter") onEnter?.(); -+ } -+ }) -+ ); -+} -+function AllowedSenders({ connector, actions, run, busy }) { -+ const [openId, setOpenId] = (0, import_react39.useState)(""); -+ const allowed = connector.allowed ?? []; -+ const refused = connector.inbound?.refusedSenders ?? []; -+ const allow = (id) => void run(actions.connectorAllow(connector.id, id)); -+ return (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-allowed" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")), -+ allowed.length === 0 && refused.length === 0 ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null, -+ allowed.map((entry) => (0, import_react39.createElement)( -+ "div", -+ { key: entry.openId, className: "bt-connector-person" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block")) -+ )), -+ refused.map((entry) => (0, import_react39.createElement)( -+ "div", -+ { key: entry.openId, className: "bt-connector-person" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow")) -+ )), -+ (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_react39.createElement)("input", { -+ className: "bt-input", -+ type: "text", -+ value: openId, -+ spellCheck: false, -+ "aria-label": t("Feishu open_id"), -+ placeholder: t("Paste an open_id to allow"), -+ onChange: (event) => setOpenId(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Enter" && openId.trim() !== "") { -+ allow(openId.trim()); -+ setOpenId(""); -+ } -+ } -+ }), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => { -+ allow(openId.trim()); -+ setOpenId(""); -+ } }, t("Allow")) -+ ) -+ ); -+} -+var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; -+function AppChip({ name, status, onRemove }) { -+ const label = APP_STATUS[status]?.(); -+ return (0, import_react39.createElement)( -+ "button", -+ { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, -+ name, -+ label ? (0, import_react39.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null -+ ); -+} -+function AppPicker({ apps, onChange, search, statuses = {} }) { -+ const [query, setQuery] = (0, import_react39.useState)(""); -+ const [found, setFound] = (0, import_react39.useState)(null); -+ const [failed, setFailed] = (0, import_react39.useState)(null); -+ const names = (0, import_react39.useRef)(new Map(APPS)); -+ const asked = (0, import_react39.useRef)(0); -+ (0, import_react39.useEffect)(() => { -+ const text2 = query.trim(); -+ if (text2 === "" || search === void 0) { -+ setFound(null); -+ setFailed(null); -+ return void 0; - } -- if (tag === "canvas") { -- const box = el.getBoundingClientRect(); -- return box.width >= 200 && box.height >= 100 ? "\n\n[canvas: drawn content, not readable]\n\n" : ""; -+ const ask = ++asked.current; -+ const timer = setTimeout(() => { -+ search(text2).then((list) => { -+ if (ask !== asked.current) return; -+ for (const app of list) names.current.set(app.slug, app.name); -+ setFound(list); -+ setFailed(null); -+ }, (failure) => { -+ if (ask !== asked.current) return; -+ setFound(null); -+ setFailed(failure?.message ?? String(failure)); -+ }); -+ }, 250); -+ return () => clearTimeout(timer); -+ }, [query, search]); -+ const nameOf = (slug) => names.current.get(slug) ?? appName(slug); -+ const picked = new Set(apps); -+ const needle = query.trim().toLowerCase(); -+ const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); -+ const results = (found ?? local).filter((app) => !picked.has(app.slug)); -+ return (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-apps" }, -+ apps.length > 0 ? (0, import_react39.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react39.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, -+ (0, import_react39.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), -+ failed ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, -+ (0, import_react39.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react39.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { -+ names.current.set(app.slug, app.name); -+ onChange([...apps, app.slug]); -+ } }, app.name))) -+ ); -+} -+function AppStatusList({ apps, statuses }) { -+ if (apps.length === 0 || Object.keys(statuses).length === 0) return null; -+ return (0, import_react39.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react39.createElement)( -+ "li", -+ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), -+ (0, import_react39.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") -+ ))); -+} -+function ConnectorRow({ connector, roster, actions }) { -+ const [form, setForm] = (0, import_react39.useState)(false); -+ const [bots, setBots] = (0, import_react39.useState)(false); -+ const [token, setToken] = (0, import_react39.useState)(""); -+ const [apps, setApps] = (0, import_react39.useState)(() => connector.apps ?? []); -+ const [picking, setPicking] = (0, import_react39.useState)(false); -+ const [busy, setBusy] = (0, import_react39.useState)(false); -+ const [error, setError] = (0, import_react39.useState)(null); -+ const [message, setMessage] = (0, import_react39.useState)(""); -+ const run = (task, after) => { -+ setBusy(true); -+ setError(null); -+ return task.then(() => { -+ after?.(); -+ }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -+ }; -+ const gateway = connector.id === "composio"; -+ const sender = connector.kind === "sender"; -+ const channel = connector.kind === "channel"; -+ const modes = connector.modes ?? []; -+ const [mode, setMode] = (0, import_react39.useState)(() => connector.mode ?? modes[0]?.id); -+ const [values, setValues] = (0, import_react39.useState)({}); -+ const chosen = modes.find((entry) => entry.id === mode) ?? modes[0]; -+ const field = (name) => values[name] ?? ""; -+ const setField = (name, value) => setValues((current) => ({ ...current, [name]: value })); -+ const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== ""); -+ const [statuses, setStatuses] = (0, import_react39.useState)({}); -+ const appsKey = (connector.apps ?? []).join(","); -+ (0, import_react39.useEffect)(() => { -+ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { -+ setStatuses({}); -+ return void 0; - } -- if (tag === "iframe" || tag === "frame") { -- const box = el.getBoundingClientRect(); -- return tag === "iframe" && (box.width < 2 || box.height < 2) ? "" : frame(el, next); -+ let live = true; -+ actions.connectorAppStatus(connector.id).then((value) => { -+ if (live) setStatuses(value ?? {}); -+ }, () => { -+ if (live) setStatuses({}); -+ }); -+ return () => { -+ live = false; -+ }; -+ }, [gateway, connector.state, appsKey, picking]); -+ const search = (0, import_react39.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); -+ const connect = () => { -+ if (busy) return; -+ if (channel) { -+ if (chosen === void 0 || !filled) return; -+ const credentials = Object.fromEntries(chosen.credentials.map((credential) => [credential.name, field(credential.name)])); -+ void run(actions.connectorConnect(connector.id, { mode: chosen.id, credentials }), () => { -+ setValues({}); -+ setForm(false); -+ }); -+ return; - } -- const found = itemFor(el, style); -- const mark = found ? `[#${found.number}]` : ""; -- if (tag === "input" || tag === "textarea" || tag === "select") { -- if (found) found.item.name = nameOf(el, tag === "select" ? "" : el.labels?.[0] ? collapse(el.labels[0].textContent) : "", found.secret); -- return mark ? ` ${mark} ` : ""; -+ if (token.trim() === "") return; -+ if (gateway && apps.length === 0) { -+ setError(t("Pick at least one app")); -+ return; - } -- let body2; -- if (tag === "pre") { -- const code = shown ? String(el.innerText || el.textContent || "") : ""; -- chars += code.length; -- body2 = code.trim() ? keep(fenced(code, langOf(el))) : ""; -- } else if (tag === "table") { -- body2 = tableMarkdown(el, next); -- } else if (tag === "ul" || tag === "ol") { -- body2 = list(el, next); -- } else if (tag === "details" && !el.open) { -- const summary = [...el.children].find((child) => child.localName === "summary"); -- body2 = summary ? render(summary, next) : ""; -- } else { -- body2 = children(el, { ...next, pre: keepsSpace }); -+ void run(actions.connectorConnect(connector.id, token, gateway ? apps : void 0), () => { -+ setToken(""); -+ setForm(false); -+ }); -+ }; -+ const saveApps = () => { -+ if (busy) return; -+ if (apps.length === 0) { -+ setError(t("Pick at least one app")); -+ return; - } -- if (found) found.item.name = nameOf(el, oneLine(body2), found.secret); -- const level = /^h([1-6])$/.exec(tag)?.[1] || (el.getAttribute("role") === "heading" ? el.getAttribute("aria-level") || "2" : ""); -- if (level) { -- const title = oneLine(body2); -- return title ? ` -+ void run(actions.connectorApps(connector.id, apps), () => setPicking(false)); -+ }; -+ const on = connector.state !== "off"; -+ const canPost = sender || channel && connector.mode === "webhook"; -+ const perBot = !sender && (!channel || connector.mode === "app"); -+ const Mark = MARKS[connector.id]; -+ const row = (0, import_react39.useRef)(null); -+ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`); -+ return (0, import_react39.createElement)( -+ "div", -+ { ref: row, className: "bt-connector", "data-state": connector.state }, -+ (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-head" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react39.createElement)(Mark) : null), -+ (0, import_react39.createElement)( -+ "span", -+ { className: "bt-connector-text" }, -+ (0, import_react39.createElement)("span", { className: "bt-connector-name" }, connector.name), -+ (0, import_react39.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) -+ ), -+ (0, import_react39.createElement)( -+ "span", -+ { className: "bt-connector-actions" }, -+ connector.state === "error" ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, -+ on && gateway && !picking ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ setApps(connector.apps ?? []); -+ setPicking(true); -+ } }, t("Choose apps")) : null, -+ on && perBot ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, -+ on ? (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) -+ ) -+ ), -+ on && gateway && !picking ? (0, import_react39.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, -+ on && perBot ? (0, import_react39.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, -+ on && bots ? (0, import_react39.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, -+ on && channel && connector.mode === "app" ? (0, import_react39.createElement)(AllowedSenders, { connector, actions, run, busy }) : null, -+ on && canPost ? (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form" }, -+ (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_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")) -+ ) -+ ) : null, -+ !on && form ? (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form" }, -+ channel ? (0, import_react39.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react39.createElement)("button", { -+ key: entry.id, -+ type: "button", -+ className: "bt-state", -+ "aria-pressed": entry.id === chosen?.id, -+ onClick: () => { -+ setMode(entry.id); -+ setValues({}); -+ setError(null); -+ } -+ }, t(entry.label)))) : null, -+ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react39.createElement)(CredentialField, { -+ key: credential.name, -+ credential, -+ value: field(credential.name), -+ onChange: (value) => setField(credential.name, value), -+ onEnter: connect, -+ autoFocus: index === 0 -+ })) : (0, import_react39.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react39.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), -+ gateway ? (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, -+ (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, -+ (0, import_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" }, -+ channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub") -+ ), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ setForm(false); -+ setToken(""); -+ setValues({}); -+ setError(null); -+ } }, t("Cancel")), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) -+ ) -+ ) : null, -+ on && gateway && picking ? (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form" }, -+ (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), -+ (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), -+ (0, import_react39.createElement)( -+ "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, onClick: () => { -+ setPicking(false); -+ setError(null); -+ } }, t("Cancel")), -+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) -+ ) -+ ) : null, -+ error ? (0, import_react39.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null -+ ); -+} -+function ConnectorsPage({ roster, actions }) { -+ const list = roster?.connectors ?? []; -+ return (0, import_react39.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react39.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react39.createElement)("div", { className: "bt-set-row" }, (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react39.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) -+ ); -+} -+ -+// src/client/feedback.js -+var import_react40 = require("react"); -+var FeedbackIcon = () => (0, import_react40.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_react40.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), -+ (0, import_react40.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) -+); -+function download(name, text2) { -+ const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); -+ const anchor = document.createElement("a"); -+ anchor.href = url; -+ anchor.download = name; -+ anchor.click(); -+ setTimeout(() => URL.revokeObjectURL(url), 1e3); -+} -+var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); -+function whenText2(time) { -+ const date = new Date(time); -+ const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); -+ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -+ return today ? clock4 : `${date.toLocaleDateString(dateLocale(), { month: "short", day: "numeric" })} ${clock4}`; -+} -+function entrySummary(entry) { -+ if (entry.source === "turn") return failureLabel(entry.code, entry.text); -+ return entry.text; -+} -+var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" }; -+function FeedbackPage({ actions, run, Section, Row: Row4 }) { -+ const [what, setWhat] = (0, import_react40.useState)(""); -+ const [report, setReport] = (0, import_react40.useState)(null); -+ 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) => { -+ setReport(next); -+ return next; -+ }); -+ const load = () => fetchReport().then(() => void 0); -+ (0, import_react40.useEffect)(() => { -+ void run(load()); -+ }, []); -+ const copyReport = () => run(fetchReport().then(async (next) => { -+ const copied = await copy(next.markdown); -+ setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); -+ if (!copied) setPreview(true); -+ })); -+ const [issueLink, setIssueLink] = (0, import_react40.useState)(null); -+ const openIssue = () => { -+ const win = window.open("", "_blank"); -+ if (win !== null) win.opener = null; -+ run(fetchReport().then(async (next) => { -+ const copied = await copy(next.markdown); -+ const { url, inline } = issueUrl(next.issuesUrl, { what, diagnostics: next.markdown }); -+ if (win !== null) { -+ win.location.href = url; -+ setIssueLink(null); -+ setNote(inline ? t("Opened a new GitHub issue with the diagnostics filled in. Check it before you submit.") : copied ? t("Opened a new GitHub issue. The diagnostics are long, so they are on the clipboard: paste them into the issue.") : t("Opened a new GitHub issue. Copy the diagnostics below into it.")); -+ } else { -+ setIssueLink(url); -+ setNote(t("The browser blocked the new window.")); -+ } -+ if (!inline && !copied) setPreview(true); -+ })); -+ }; -+ const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { -+ download("ds-bot-diagnostics.log", text2 || "No entries.\n"); -+ 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.")))); -+ 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); -+ return [ -+ (0, import_react40.createElement)( -+ Section, -+ { key: "report", title: t("Report a problem") }, -+ (0, import_react40.createElement)( -+ "div", -+ { className: "bt-set-row bt-set-paste bt-fb-what" }, -+ (0, import_react40.createElement)( -+ "label", -+ { className: "bt-set-copy", htmlFor: "bt-fb-what" }, -+ (0, import_react40.createElement)("span", null, t("What happened?")), -+ (0, import_react40.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) -+ ), -+ (0, import_react40.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) -+ ), -+ (0, import_react40.createElement)( -+ Row4, -+ { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) -+ ), -+ preview ? (0, import_react40.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react40.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, -+ (0, import_react40.createElement)( -+ Row4, -+ { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) -+ ) -+ ), -+ note ? (0, import_react40.createElement)( -+ "div", -+ { key: "note", className: "bt-note bt-fb-note", role: "status" }, -+ note, -+ issueLink ? [" ", (0, import_react40.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null -+ ) : null, -+ (0, import_react40.createElement)( -+ Section, -+ { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, -+ shown.length === 0 ? (0, import_react40.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react40.createElement)( -+ "div", -+ { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, -+ (0, import_react40.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), -+ (0, import_react40.createElement)( -+ "div", -+ { className: "bt-set-copy" }, -+ (0, import_react40.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), -+ (0, import_react40.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) -+ ), -+ entry.code ? (0, import_react40.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null -+ )), -+ (0, import_react40.createElement)( -+ Row4, -+ { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) -+ ) -+ ) -+ ]; -+} -+var TOAST_MS = 6e3; -+function ErrorToast({ useUi, actions }) { -+ const toast = useUi((state) => state.toast); -+ const [leaving, setLeaving] = (0, import_react40.useState)(false); -+ (0, import_react40.useEffect)(() => { -+ if (!toast) return void 0; -+ setLeaving(false); -+ const fade = setTimeout(() => setLeaving(true), TOAST_MS); -+ const gone = setTimeout(() => actions.dismissToast(toast.token), TOAST_MS + 200); -+ return () => { -+ clearTimeout(fade); -+ clearTimeout(gone); -+ }; -+ }, [toast?.token]); -+ if (!toast) return null; -+ const info = toast.tone === "info"; -+ return (0, import_react40.createElement)( -+ "div", -+ { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, -+ (0, import_react40.createElement)("span", { className: "bt-toast-text" }, toast.text), -+ info ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { -+ actions.dismissToast(toast.token); -+ actions.openSettings("feedback"); -+ } }, t("Details")), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react40.createElement)(CloseIcon)) -+ ); -+} -+var FEEDBACK_CSS = ` -+.bt-fb-what{cursor:default} -+.bt-fb-what:hover,.bt-fb-entry:hover,.bt-fb-preview:hover{background:none} -+.bt-fb-what .bt-input-area{min-height:80px} -+.bt-fb-preview{cursor:default;padding:0} -+.bt-fb-preview pre{margin:0;width:100%;max-height:240px;overflow:auto;padding:10px 12px;box-sizing:border-box;font:11px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--bt-ink-2);white-space:pre-wrap;word-break:break-word;outline:none} -+.bt-fb-primary{height:28px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);padding:0 14px;font-size:12px;cursor:pointer;transition:filter .12s ease} -+.bt-fb-primary:hover{filter:brightness(1.08)} -+.bt-fb-primary:active{filter:brightness(.94)} -+.bt-fb-note{margin:-8px 4px 0} -+.bt-fb-entry{cursor:default;align-items:flex-start} -+.bt-fb-dot{flex:none;width:6px;height:6px;border-radius:50%;margin-top:6px;background:#e02135} -+.bt-fb-dot[data-level=warn]{background:#e3a008} -+.bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} -+.bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} -+.bt-event-error{color:#e02135} -+.bt-toast{position:fixed;left:0;right:0;bottom:28px;z-index:1000;display:flex;align-items:center;gap:8px;width:fit-content;max-width:min(560px,calc(100vw - 32px));margin-inline:auto;padding:8px 8px 8px 14px;border-radius:12px;background:var(--bt-card,#fff);color:var(--bt-ink,#141414);border:1px solid var(--bt-line-2,rgba(0,0,0,.15));box-shadow:0 8px 28px rgba(0,0,0,.14);font-size:13px;line-height:18px;animation:bt-toast-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} -+.bt-toast[data-leaving]{opacity:0;transform:translateY(6px) scale(.98);filter:blur(3px);transition:opacity .18s ease-in,transform .18s ease-in,filter .18s ease-in} -+.bt-toast-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical} -+.bt-toast-text::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#e02135;margin:0 8px 1px 0} -+.bt-toast-link{flex:none;border:0;background:none;color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;padding:4px 6px;border-radius:6px} -+.bt-toast-link:hover{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-toast-close{flex:none} -+@keyframes bt-toast-in{from{transform:translateY(16px) scale(.94)}} -+@media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}} -+`; - --${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} -+// src/client/about.js -+var import_react41 = require("react"); -+var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); -+function botHint(update) { -+ if (update.phase === "installing") return t("Updating FlyBot core…"); -+ if (update.phase === "installed") return t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }); -+ if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") }); -+ if (update.available) return t("FlyBot core {version} is available", { version: update.latest }); -+ if (update.enabled === false) return t("Update checks are off."); -+ if (update.source === "path") return t("Installed from a local folder; update it there."); -+ if (update.source === "git" && update.build?.branch !== void 0) return branchHint(update); -+ if (update.source === "git" && update.build?.pinned) return t("Pinned to commit {commit}. Updates come from the maintainer: reinstall FlyBot to get them.", { commit: update.build.commit }); -+ if (!update.checkedAt) return null; -+ if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install."); -+ const checked = t("Checked {time}", { time: whenText2(update.checkedAt) }); -+ return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`; -+} -+function branchHint({ build, behind, checkedAt }) { -+ const where = [build.branch, build.commit].filter(Boolean).join(" "); -+ if (build.commit === void 0 || !checkedAt) return t("Installed from GitHub ({where}). Updates come from the maintainer: reinstall FlyBot to get them.", { where }); -+ const checked = t("Checked {time}", { time: whenText2(checkedAt) }); -+ const state = behind === null ? t("Could not compare with {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : behind === 0 ? t("Up to date with {branch}.", { branch: build.branch }) : behind === 1 ? t("1 commit behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { branch: build.branch }) : t("{count} commits behind {branch}. Updates come from the maintainer: reinstall FlyBot to get them.", { count: behind, branch: build.branch }); -+ return `${state} · ${checked}`; -+} -+var versionLine = (update) => [t("Version {version}", { version: update.current }), [update.build?.branch, update.build?.commit].filter(Boolean).join(" ")].filter(Boolean).join(" · "); -+function dshHint(dsh, shown) { -+ if (dsh.phase === "installing") return t("Updating DSH…"); -+ if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }); -+ if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") }); -+ if (dsh.available === false && dsh.blockedBy) return t("Update FlyBot core {version} first.", { version: dsh.blockedBy }); -+ if (shown) return t("DSH {version} is available", { version: dsh.verified }); -+ return t("Version {version}", { version: dsh.current }); -+} -+var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); -+function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) { -+ const update = useUpdate((value) => value) ?? {}; -+ const [checking, setChecking] = (0, import_react41.useState)(false); -+ const [stars, setStars] = (0, import_react41.useState)(null); -+ (0, import_react41.useEffect)(() => { -+ let live = true; -+ actions.repoStars().then((count) => { -+ if (live && Number.isInteger(count)) setStars(count); -+ }, () => { -+ }); -+ return () => { -+ live = false; -+ }; -+ }, []); -+ const check = () => { -+ setChecking(true); -+ void run(actions.checkUpdate()).then(() => { -+ setChecking(false); -+ }); -+ }; -+ const phase = update.phase ?? "idle"; -+ const dsh = update.dsh; -+ const dshPhase = dsh?.phase ?? "idle"; -+ const dshNew = showsDshRow(dsh, update.app); -+ const dshShown = dshNew && dsh.available; -+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react41.createElement)( -+ "button", -+ { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, -+ phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") -+ ) : (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); -+ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react41.createElement)( -+ "button", -+ { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, -+ dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") -+ ) : null; -+ const repo = update.repoUrl; -+ return [ -+ (0, import_react41.createElement)( -+ "div", -+ { key: "hero", className: "bt-about-hero" }, -+ (0, import_react41.createElement)("span", { className: "bt-about-mark" }, (0, import_react41.createElement)(FlyBotMark, { size: 52 })), -+ (0, import_react41.createElement)( -+ "div", -+ { className: "bt-about-copy" }, -+ (0, import_react41.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), -+ update.current ? (0, import_react41.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null -+ ), -+ repo ? (0, import_react41.createElement)( -+ "a", -+ { -+ className: "bt-star", -+ href: repo, -+ target: "_blank", -+ rel: "noopener noreferrer", -+ title: t("Star FlyBot on GitHub"), -+ "aria-label": stars === null ? t("Star FlyBot on GitHub") : `${t("Star FlyBot on GitHub")} · ${t("{count} stars", { count: stars })}` -+ }, -+ (0, import_react41.createElement)("span", { className: "bt-star-main" }, (0, import_react41.createElement)("span", { className: "bt-star-icon" }, (0, import_react41.createElement)(StarLineIcon)), "Star"), -+ stars === null ? null : (0, import_react41.createElement)("span", { className: "bt-star-count" }, starCount(stars)) -+ ) : null -+ ), -+ repo ? (0, import_react41.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, -+ (0, import_react41.createElement)( -+ Section, -+ { key: "updates", title: t("Updates") }, -+ (0, import_react41.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react41.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), -+ dsh?.current ? (0, import_react41.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null -+ ), -+ (0, import_react41.createElement)( -+ Section, -+ { key: "links", title: t("Links") }, -+ repo ? (0, import_react41.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react41.createElement)("a", linkProps(repo), (0, import_react41.createElement)(GitHubMark), t("Open"))) : null, -+ update.notesUrl ? (0, import_react41.createElement)(Row4, { label: t("Release notes") }, (0, import_react41.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, -+ (0, import_react41.createElement)(Row4, { label: t("Report a problem") }, (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) -+ ) -+ ]; -+} -+var ABOUT_CSS = ` -+.bt-about-hero{display:flex;align-items:center;gap:14px;padding:16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} -+.bt-about-mark{flex:none;display:inline-flex} -+.bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} -+.bt-about-name{font-size:16px;line-height:22px;font-weight:600} -+.bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -+.bt-about-ask{margin:-10px 4px 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} -+.bt-star{flex:none;display:inline-flex;align-items:stretch;height:30px;border-radius:999px;border:1px solid var(--bt-line-2,var(--bt-line));overflow:hidden;color:var(--bt-ink);text-decoration:none;font-size:12px;line-height:28px;transition:transform .12s ease,border-color .15s ease} -+.bt-star:hover{border-color:color-mix(in srgb,#e3a008 55%,var(--bt-line))} -+.bt-star:active{transform:scale(.96)} -+.bt-star-main{display:inline-flex;align-items:center;gap:6px;padding:0 12px 0 10px;background:var(--bt-hover);font-weight:500} -+.bt-star-icon{display:inline-flex;color:var(--bt-ink-2);transition:color .15s ease} -+.bt-star-icon path{transition:fill .15s ease} -+.bt-star:hover .bt-star-icon{color:#e3a008;animation:bt-star-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+.bt-star:hover .bt-star-icon path{fill:currentColor} -+.bt-star-count{display:inline-flex;align-items:center;padding:0 11px;border-left:1px solid var(--bt-line-2,var(--bt-line));color:var(--bt-ink-2);font-variant-numeric:tabular-nums;animation:bt-star-count-in ${SPRING_POP.duration}ms ${SPRING_POP.easing},bt-veil-in .2s ease-out} -+@keyframes bt-star-pop{from{scale:.6;rotate:-30deg}} -+@keyframes bt-star-count-in{from{translate:-6px 0}} -+.bt-about-link{display:inline-flex;align-items:center;gap:6px;text-decoration:none} -+.bt-about-link svg{width:14px;height:14px} -+.bt-fb-primary:disabled{opacity:.6;cursor:default;filter:none} -+@media (prefers-reduced-motion:reduce){.bt-star:hover .bt-star-icon,.bt-star-count{animation:none}} -+`; - --` : mark; -- } -- if (tag === "code" && !ctx.pre) { -- const code = oneLine(body2); -- if (!code) return mark; -- const ticks = code.includes("`") ? "``" : "`"; -- return `${ticks}${code}${ticks}${mark}`; -- } -- if (tag === "blockquote") { -- const quoted = block(body2); -- return quoted ? keep(quoted.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n")) : ""; -- } -- const end = body2.trimEnd().length; -- const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -- if (ctx.pre && !BLOCK_DISPLAY.test(display)) return marked; -- if (keepsSpace && BLOCK_DISPLAY.test(display) && tag !== "pre") { -- return keep(expand(marked).replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/^\n+|\s+$/g, ""), ""); -+// src/client/settings.js -+var import_react42 = require("react"); -+var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); -+var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["projects", "Projects"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["workflows", "Workflows"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; -+var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; -+function SetSection({ title, children }) { -+ return (0, import_react42.createElement)("section", { className: "bt-set-section" }, (0, import_react42.createElement)("h3", null, title), (0, import_react42.createElement)("div", { className: "bt-set-card" }, children)); -+} -+function SetRow({ label, hint, children }) { -+ return (0, import_react42.createElement)( -+ "div", -+ { className: "bt-set-row" }, -+ (0, import_react42.createElement)("div", { className: "bt-set-copy" }, (0, import_react42.createElement)("span", null, label), hint ? (0, import_react42.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ children ? (0, import_react42.createElement)("div", { className: "bt-set-control" }, children) : null -+ ); -+} -+var NOTIFY_KINDS = [ -+ ["approval", "Waiting for approval", "Once more if it still waits after 15 min."], -+ ["finished", "Finished"], -+ ["failed", "Failed"] -+]; -+function GeneralPage({ roster, actions, run }) { -+ const prefs = roster.prefs ?? {}; -+ const themes = allThemes(); -+ const current = themes[prefs.theme] ? prefs.theme : "deepseek"; -+ const [custom, setCustom] = (0, import_react42.useState)(prefs.accent ?? "#4d6bfe"); -+ const timer = (0, import_react42.useRef)(0); -+ (0, import_react42.useEffect)(() => () => clearTimeout(timer.current), []); -+ const pickCustom = (value) => { -+ setCustom(value); -+ clearTimeout(timer.current); -+ timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350); -+ }; -+ return [ -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "look", title: t("Appearance") }, -+ (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, -+ (0, import_react42.createElement)( -+ "select", -+ { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, -+ Object.entries(themes).map(([id, theme]) => (0, import_react42.createElement)("option", { key: id, value: id }, t(theme.label))) -+ ) -+ ), -+ (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-swatches" }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), -+ ACCENT_INKS.map((ink) => (0, import_react42.createElement)("button", { -+ key: ink, -+ type: "button", -+ className: "bt-swatch", -+ title: t(capitalize2(ink)), -+ "aria-label": t(capitalize2(ink)), -+ "aria-pressed": prefs.accent === INKS[ink][0], -+ style: { background: INKS[ink][0] }, -+ onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) -+ })), -+ (0, import_react42.createElement)( -+ "label", -+ { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, -+ (0, import_react42.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) -+ ) -+ ) -+ ) -+ ), -+ // One switch for every system notification (#353), then one per kind (#469). -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "notify", title: t("Notifications") }, -+ (0, import_react42.createElement)(SwitchRow, { -+ label: t("In other chats"), -+ hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), -+ on: prefs.notifications !== false, -+ onChange: (on) => run(actions.setPrefs({ notifications: on })) -+ }), -+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react42.createElement)(SwitchRow, { -+ key: kind, -+ label: t(label), -+ hint: hint ? t(hint) : void 0, -+ disabled: prefs.notifications === false, -+ on: prefs.notify?.[kind] !== false, -+ onChange: (on) => run(actions.setPrefs({ notify: { [kind]: on } })) -+ })) -+ ) -+ ]; -+} -+function MainBotsSection({ roster, actions, run }) { -+ const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean); -+ const [first, ...others] = mains; -+ const own = roster.bots.filter((bot) => !bot.remote); -+ const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); -+ return (0, import_react42.createElement)( -+ SetSection, -+ { title: t("Main Bot") }, -+ (0, import_react42.createElement)( -+ SetRow, -+ { -+ label: t("Main Bot"), -+ hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 -+ }, -+ (0, import_react42.createElement)("select", { -+ className: "bt-select", -+ "aria-label": t("Main Bot"), -+ value: first?.id ?? "", -+ onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) -+ }, first ? null : (0, import_react42.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react42.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ ), -+ others.map((bot) => (0, import_react42.createElement)( -+ SetRow, -+ { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) -+ )), -+ candidates.length > 0 ? (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, -+ (0, import_react42.createElement)("select", { -+ className: "bt-select", -+ "aria-label": t("Add a Main Bot"), -+ value: "", -+ onChange: (event) => { -+ if (event.target.value) void run(actions.setMain(event.target.value, true)); -+ } -+ }, (0, import_react42.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react42.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ ) : null -+ ); -+} -+var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]]; -+function MotionSection({ roster, actions, run }) { -+ const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; -+ return (0, import_react42.createElement)( -+ SetSection, -+ { title: t("Animation") }, -+ (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Bot animation") }, -+ (0, import_react42.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react42.createElement)("button", { -+ key: id, -+ type: "button", -+ "aria-pressed": motion === id, -+ onClick: () => { -+ if (motion !== id) run(actions.setPrefs({ motion: id })); -+ } -+ }, t(label)))) -+ ) -+ ); -+} -+function BotsPage({ roster, actions, run }) { -+ const [creating, setCreating] = (0, import_react42.useState)(false); -+ return [ -+ (0, import_react42.createElement)(MainBotsSection, { key: "main", roster, actions, run }), -+ (0, import_react42.createElement)(MotionSection, { key: "motion", roster, actions, run }), -+ creating ? (0, import_react42.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react42.createElement)( -+ "div", -+ { key: "new", className: "bt-actions" }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react42.createElement)(PlusIcon), t("New Bot")) -+ ), -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, -+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react42.createElement)( -+ "button", -+ { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, -+ (0, import_react42.createElement)( -+ "span", -+ { className: "bt-set-bot" }, -+ (0, import_react42.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), -+ (0, import_react42.createElement)("span", { className: "bt-row-title" }, bot.name), -+ bot.role ? (0, import_react42.createElement)("span", { className: "bt-tag" }, bot.role) : null, -+ isMainOf(roster, bot.id) ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, -+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ ), -+ (0, import_react42.createElement)("span", { className: "bt-chevron" }, (0, import_react42.createElement)(ChevronRightIcon)) -+ )) -+ ) -+ ]; -+} -+function GroupsPage({ roster, actions, run }) { -+ const [creating, setCreating] = (0, import_react42.useState)(false); -+ return [ -+ creating ? (0, import_react42.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react42.createElement)( -+ "div", -+ { key: "new", className: "bt-actions" }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react42.createElement)(PlusIcon), t("Create group chat")) -+ ), -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, -+ roster.rooms.length === 0 ? (0, import_react42.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { -+ const admin = roster.byId[room2.admin]; -+ return (0, import_react42.createElement)( -+ "button", -+ { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, -+ (0, import_react42.createElement)( -+ "span", -+ { className: "bt-set-bot" }, -+ (0, import_react42.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -+ (0, import_react42.createElement)("span", { className: "bt-row-title" }, room2.name), -+ (0, import_react42.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), -+ admin ? (0, import_react42.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react42.createElement)(AdminStar), admin.name) : null, -+ room2.hidden ? (0, import_react42.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ ), -+ (0, import_react42.createElement)("span", { className: "bt-chevron" }, (0, import_react42.createElement)(ChevronRightIcon)) -+ ); -+ }) -+ ) -+ ]; -+} -+function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { -+ const [error, setError] = (0, import_react42.useState)(""); -+ const close = (0, import_react42.useCallback)(() => actions.closeSettings(), []); -+ useEscape(close); -+ const run = (promise) => Promise.resolve(promise).then(() => { -+ setError(""); -+ return true; -+ }).catch((failure) => { -+ setError(failure?.message ?? String(failure)); -+ return false; -+ }); -+ const bot = page === "bots" && id ? roster.byId[id] : void 0; -+ const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; -+ const extra = useSettingsPages().map((entry) => ({ entry, key: `plugin:${entry.id}` })); -+ const pages = SETTINGS_PAGES.flatMap((item) => item[0] === "connectors" ? [item, ...extra.map(({ entry, key }) => [key, entry.label ?? entry.id])] : [item]); -+ const shown = pages.some(([key]) => key === page) ? page : "general"; -+ const pluginPage = extra.find(({ key }) => key === shown)?.entry; -+ const usageFocus = shown === "usage" && id ? id : null; -+ const label = t(pages.find(([key]) => key === shown)[1]); -+ const back = bot ? t("Back to Bots") : room2 ? t("Back to group chats") : usageFocus ? t("All usage") : null; -+ const nav = useGlide(shown, { axis: "y" }); -+ const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; -+ const depth = bot || room2 ? 1 : 0; -+ const last = (0, import_react42.useRef)({ key: pageKey, depth, move: "up" }); -+ if (last.current.key !== pageKey) { -+ last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; -+ } -+ const [usageOpened, setUsageOpened] = (0, import_react42.useState)(shown === "usage"); -+ if (shown === "usage" && !usageOpened) setUsageOpened(true); -+ const errorLine = error ? (0, import_react42.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -+ return (0, import_react42.createElement)( -+ "div", -+ { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react42.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, -+ (0, import_react42.createElement)( -+ "nav", -+ { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, -+ (0, import_react42.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), -+ pages.map(([key, text2]) => (0, import_react42.createElement)("button", { -+ key, -+ type: "button", -+ className: "bt-settings-item", -+ "aria-current": shown === key ? "page" : void 0, -+ onClick: () => actions.openSettings(key) -+ }, (0, import_react42.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))), -+ // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot -+ // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). -+ (0, import_react42.createElement)("button", { -+ type: "button", -+ className: "bt-settings-item bt-settings-shell", -+ title: t("Opens DeepSeek Harness's own settings (models, providers, permissions) in a separate window"), -+ "aria-label": t("Harness settings, opens DeepSeek Harness's own settings window"), -+ onClick: () => { -+ close(); -+ openHarnessSettings(); -+ } -+ }, (0, import_react42.createElement)(SlidersIcon), t("Harness settings"), (0, import_react42.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) -+ ), -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-settings-main" }, -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-settings-head" }, -+ back ? (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react42.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react42.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) -+ ), -+ shown === "usage" ? null : (0, import_react42.createElement)( -+ "div", -+ { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, -+ errorLine, -+ shown === "general" ? (0, import_react42.createElement)(GeneralPage, { roster, actions, run }) : null, -+ bot ? (0, import_react42.createElement)("div", { className: "bt-settings-form" }, (0, import_react42.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, -+ shown === "bots" && !bot ? (0, import_react42.createElement)(BotsPage, { roster, actions, run }) : null, -+ room2 ? (0, import_react42.createElement)("div", { className: "bt-settings-form" }, (0, import_react42.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, -+ shown === "groups" && !room2 ? (0, import_react42.createElement)(GroupsPage, { roster, actions, run }) : null, -+ shown === "projects" ? (0, import_react42.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null, -+ shown === "connectors" ? (0, import_react42.createElement)(ConnectorsPage, { roster, actions }) : null, -+ shown === "workflows" ? (0, import_react42.createElement)(WorkflowsPage, { call: call2, roster }) : null, -+ pluginPage ? (0, import_react42.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, -+ shown === "capabilities" ? (0, import_react42.createElement)(CapabilitiesPage, { roster, actions, run }) : null, -+ shown === "schedules" ? (0, import_react42.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, -+ shown === "secrets" ? (0, import_react42.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, -+ shown === "feedback" ? (0, import_react42.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, -+ shown === "about" ? (0, import_react42.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null -+ ), -+ usageOpened ? (0, import_react42.createElement)( -+ "div", -+ { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, -+ shown === "usage" ? errorLine : null, -+ (0, import_react42.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) -+ ) : null -+ ), -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react42.createElement)(CloseIcon)) -+ ) -+ ); -+} -+var openProjectBoard = (id) => { -+ if (typeof window !== "undefined") window.location.hash = `#project=${id}`; -+}; -+var projectMembers = (roster) => [ -+ ...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })), -+ ...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null })) -+]; -+function ProjectsList({ projects, roster, call: call4, reload }) { -+ const members = projectMembers(roster); -+ const bind = async (choice, project) => { -+ const [kind, id] = String(choice ?? "").split(":"); -+ if (!kind || !id) return; -+ await call4("project-bind", { kind, id, projectId: project.id }); -+ await reload?.(); -+ }; -+ const unbind = async (member) => { -+ await call4("project-bind", { kind: member.kind, id: member.id, projectId: null }); -+ await reload?.(); -+ }; -+ const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0); -+ return [ -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "projects", title: t("Projects · {count}", { count: projects.length }) }, -+ projects.length === 0 ? (0, import_react42.createElement)("div", { className: "bt-set-row" }, (0, import_react42.createElement)("div", { className: "bt-set-copy" }, (0, import_react42.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react42.createElement)( -+ SetRow, -+ { -+ key: project.id, -+ label: project.name, -+ hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir -+ }, -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-project-row-controls" }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")), -+ (0, import_react42.createElement)( -+ "select", -+ { -+ className: "bt-select", -+ "aria-label": t("Bind to {name}", { name: project.name }), -+ value: "", -+ onChange: (event) => { -+ void bind(event.target.value, project); -+ } -+ }, -+ (0, import_react42.createElement)("option", { value: "" }, t("Bind a Bot or group")), -+ members.map((member) => (0, import_react42.createElement)( -+ "option", -+ { key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` }, -+ `${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}` -+ )) -+ ) -+ ) -+ )) -+ ), -+ bound2.length === 0 ? null : (0, import_react42.createElement)( -+ SetSection, -+ { key: "bindings", title: t("Bindings") }, -+ bound2.map((member) => (0, import_react42.createElement)( -+ SetRow, -+ { -+ key: `${member.kind}:${member.id}`, -+ label: member.name, -+ hint: member.kind === "group" ? t("Group") : t("Bot") -+ }, -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ void unbind(member); -+ } }, t("Unbind")) -+ )) -+ ) -+ ]; -+} -+function ProjectsPage({ roster, actions, run, call: call4 }) { -+ const [state, setState] = (0, import_react42.useState)({ projects: [], error: null }); -+ const [name, setName] = (0, import_react42.useState)(""); -+ const [workDir, setWorkDir] = (0, import_react42.useState)(null); -+ const reload = (0, import_react42.useCallback)(async () => { -+ const listing = await call4("projects"); -+ setState({ projects: listing?.projects ?? [], error: null }); -+ }, [call4]); -+ (0, import_react42.useEffect)(() => { -+ void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) })); -+ }, [reload]); -+ const create = async () => { -+ const trimmed3 = name.trim(); -+ if (trimmed3 === "") return; -+ const body = { name: trimmed3 }; -+ if (workDir !== null && workDir !== "") body.workDir = workDir; -+ try { -+ await call4("project-create", body); -+ setName(""); -+ setWorkDir(null); -+ await reload(); -+ } catch (error) { -+ setState({ projects: state.projects, error: error?.message ?? String(error) }); - } -- if (BLOCK_DISPLAY.test(display)) return ` -+ }; -+ return [ -+ state.error === null ? null : (0, import_react42.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error), -+ (0, import_react42.createElement)( -+ SetSection, -+ { key: "new", title: t("New Project") }, -+ (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Name") }, -+ (0, import_react42.createElement)("input", { -+ className: "bt-input", -+ "aria-label": t("Project name"), -+ value: name, -+ placeholder: t("e.g. Website"), -+ onChange: (event) => setName(event.target.value) -+ }) -+ ), -+ (0, import_react42.createElement)( -+ SetRow, -+ { label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") }, -+ (0, import_react42.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir }) -+ ), -+ (0, import_react42.createElement)( -+ "div", -+ { className: "bt-actions" }, -+ (0, import_react42.createElement)("button", { -+ type: "button", -+ className: "bt-primary bt-with-icon", -+ disabled: name.trim() === "", -+ onClick: () => { -+ void create(); -+ } -+ }, (0, import_react42.createElement)(PlusIcon), t("New Project")) -+ ) -+ ), -+ // The list is rendered here rather than as a child element: it holds no state of its own, -+ // and the page keeps one reload path for both halves. -+ ...ProjectsList({ projects: state.projects, roster, call: call4, reload }) -+ ]; -+} - --${marked} -+// src/client/context-menu.js -+var import_react43 = require("react"); -+function ContextMenu({ menu, roster, actions, leaving = false }) { -+ useEscape(() => actions.closeMenu()); -+ (0, import_react43.useEffect)(() => { -+ const onDown = (event) => { -+ if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); -+ }; -+ window.addEventListener("mousedown", onDown); -+ return () => window.removeEventListener("mousedown", onDown); -+ }, []); -+ const structure = useSidebarStructure(); -+ const ref = (0, import_react43.useRef)(null); -+ const [place, setPlace] = (0, import_react43.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react43.useLayoutEffect)(() => { -+ const box = ref.current?.getBoundingClientRect(); -+ if (!box) return; -+ setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -+ }, [menu.x, menu.y]); -+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react43.createElement)(AgentMenu, { menu, roster, actions, leaving }); -+ const bot = roster.byId[menu.id]; -+ const room2 = roster.roomsById[menu.id]; -+ const entry = bot ?? room2; -+ if (entry === void 0) return null; -+ const isMainBot = isMainOf(roster, menu.id); -+ const isTile = roster.mainBotId === menu.id; -+ const mainCount = (roster.mainBotIds ?? []).length; -+ const groups = bot?.remote ? [ -+ [ -+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -+ ], -+ [ -+ [t("Bot settings"), GearIcon, () => actions.openSettings("bots", bot.id)] -+ ], -+ pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]), -+ [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(currentPart(bot))]] -+ ] : [ -+ [ -+ isTile ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })], -+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -+ ], -+ [ -+ // One way to edit a Bot (#385): its name is edited in Edit Bot too. -+ bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, -+ room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, -+ room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -+ bot ? [t("Duplicate"), DuplicateIcon, async () => { -+ const copy2 = await actions.duplicateBot(bot.id); -+ actions.openSession(copy2.id); -+ }] : null, -+ bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, -+ bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null -+ ], -+ bot ? pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]) : [], -+ [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(bot ? currentPart(bot) : menu.id)]], -+ [ -+ isMainBot ? null : [t("Hide from sidebar"), HideIcon, () => actions.setFlags(menu.id, { hidden: true })], -+ isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true] -+ ] -+ ]; -+ const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); -+ return (0, import_react43.createElement)( -+ "div", -+ { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, -+ shown.map((group, index) => (0, import_react43.createElement)( -+ import_react43.Fragment, -+ { key: index }, -+ index > 0 ? (0, import_react43.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react43.createElement)("button", { -+ key: label, -+ type: "button", -+ role: "menuitem", -+ className: danger ? "bt-danger" : void 0, -+ onClick: () => { -+ actions.closeMenu(); -+ void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); -+ } -+ }, (0, import_react43.createElement)(Icon), label)) -+ )) -+ ); -+} -+function AgentMenu({ menu, roster, actions, leaving }) { -+ const ref = (0, import_react43.useRef)(null); -+ const [place, setPlace] = (0, import_react43.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react43.useLayoutEffect)(() => { -+ const box = ref.current?.getBoundingClientRect(); -+ if (!box) return; -+ setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -+ }, [menu.x, menu.y]); -+ const agent = roster.agentsById[menu.kind === "agent" ? menu.id : menu.agentId]; -+ if (agent === void 0) return null; -+ const groups = menu.kind === "agent" ? [ -+ [ -+ [agent.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(agent.id, { pinned: !agent.pinned })] -+ ], -+ [ -+ [t("Remove DSH Agent"), TrashIcon, () => actions.removeAgent(agent.id), true] -+ ] -+ ] : [ -+ [ -+ [t("Rename"), PencilIcon, () => actions.beginAgentRename(menu.id)] -+ ], -+ [ -+ [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] -+ ] -+ ]; -+ return (0, import_react43.createElement)( -+ "div", -+ { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, -+ groups.map((group, index) => (0, import_react43.createElement)( -+ import_react43.Fragment, -+ { key: index }, -+ index > 0 ? (0, import_react43.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react43.createElement)("button", { -+ key: label, -+ type: "button", -+ role: "menuitem", -+ className: danger ? "bt-danger" : void 0, -+ onClick: () => { -+ actions.closeMenu(); -+ void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); -+ } -+ }, (0, import_react43.createElement)(Icon), label)) -+ )) -+ ); -+} - --`; -- if (INLINE_BLOCK.test(display)) return ` ${marked} `; -- if (tag === "li") return ` · ${marked}`; -- return marked; -- } -- const body = document.body; -- let text2 = ""; -- if (body) { -- text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); -- } -- if (text2.length > limits.chars) { -- text2 = text2.slice(0, limits.chars); -- stop ||= `the text passed ${limits.chars} characters`; -- } -- const page = { -- url: location.href, -- title: document.title, -- lang: document.documentElement.lang || "", -- description: document.querySelector('meta[name="description"]')?.content || "", -- selection: String(getSelection() || "").slice(0, 4e3), -- format: "markdown", -- text: text2, -- items, -- stats: { ...stats, ms: Math.round(performance.now() - started) } -- }; -- if (stop) page.truncated = stop; -- return page; -+// src/client/pane-width.js -+var DEFAULT_WIDTH = 520; -+var DETAILS_WIDTH = 380; -+var DETAILS_MAX = 520; -+var MIN_WIDTH = 360; -+var MIN_CHAT = 420; -+var MIN_DETAILS = 240; -+function panelWidthRange(view, viewport) { -+ const room2 = Math.max(0, viewport - MIN_CHAT); -+ if (view === "browser") return [MIN_WIDTH, Math.max(MIN_WIDTH, room2)]; -+ const low = Math.max(MIN_DETAILS, Math.min(DETAILS_WIDTH, room2)); -+ return [low, Math.max(low, Math.min(DETAILS_MAX, room2))]; - } --var readPageScript = (limits = READ_LIMITS) => `(${readPage})(${JSON.stringify(limits)})`; --var READ_PAGE_SCRIPT = readPageScript(); - --// src/client/bot-browsers.js --var STORE_KEY = STORAGE.browsers; --var MAX_LIVE = 6; --var READ_TIMEOUT_MS = 1e4; --var PREVIEW_TIMEOUT_MS = 3e3; --var RESIZE_LATER_MS = 200; --var SEARCH_URL = "https://www.bing.com/search?q="; --var PREVIEW_SCRIPT = `(() => { -- const meta = key => document.querySelector('meta[name="' + key + '"],meta[property="' + key + '"]')?.content ?? '' -- const paragraph = [...document.querySelectorAll('main p, article p, p')].map(node => node.innerText ?? '').find(text => text.trim().length > 40) ?? '' -- const text = meta('description') || meta('og:description') || paragraph || document.body?.innerText || '' -- return text.replace(/\\s+/g, ' ').trim().slice(0, 200) --})()`; --var EMPTY = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, live: false, excerpt: "" }; --var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.internal)$/i; --var BLOCKED = "Cloud metadata addresses do not open here."; --var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); --function browserAddress(input) { -- const text2 = String(input ?? "").trim(); -- if (text2 === "") return null; -- if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; -- if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; -- const host = text2.split(/[/?#]/)[0]; -- const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); -- const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); -- if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; -- return `${SEARCH_URL}${encodeURIComponent(text2)}`; -+// src/client/storage.js -+var STORAGE = { -+ unread: "ds-bot:unread", -+ reactions: "ds-bot:reactions", -+ seen: "ds-bot:seen", -+ friendLines: "ds-bot:friend-lines", -+ prefs: "ds-bot:prefs", -+ surface: "ds-bot:surface", -+ agentSessions: "ds-bot:agent-sessions", -+ browserWidth: "ds-bot:browser-width", -+ browsers: "ds-bot.browser.v2" -+}; -+var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"]; -+var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"]; -+var LEGACY_THEMES = { grok: "mono" }; -+function legacyKeys(key) { -+ if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS; -+ const name = key.slice(key.indexOf(":") + 1); -+ return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`); - } --function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { -- const views = /* @__PURE__ */ new Map(); -- const owner = /* @__PURE__ */ new Map(); -- const tabs = createSource2({}); -- const pages = createSource2({}); -- let saved = {}; -- try { -- saved = JSON.parse(storage?.getItem(STORE_KEY) ?? "{}") ?? {}; -- } catch { -- saved = {}; -+var migrateBrowsers = (storage) => { -+ if (storage.getItem(STORAGE.browsers) !== null) { -+ for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key); -+ return; - } -- let layer; -- let shown = null; -- let rect = null; -- let visible = false; -- let resizeLater = 0; -- let disposed = false; -- let serial = 0; -- const savedTab = (botId, tabId) => saved[botId]?.tabs?.find((entry) => entry.id === tabId); -- const publish2 = (tabId, patch) => pages.set((state) => { -- const botId = owner.get(tabId); -- return { ...state, [tabId]: { ...EMPTY, ...savedTab(botId, tabId), ...state[tabId], ...patch } }; -- }); -- const writeSaved = () => { -- try { -- storage?.setItem(STORE_KEY, JSON.stringify(saved)); -- } catch { -- } -- }; -- const persist = (botId) => { -- const state = tabs.getSnapshot()[botId]; -- if (state === void 0) { -- if (saved[botId] === void 0) return; -- const { [botId]: _, ...rest } = saved; -- saved = rest; -- } else { -- const snapshot = pages.getSnapshot(); -- saved = { -- ...saved, -- [botId]: { -- tabs: state.ids.map((id) => ({ id, url: snapshot[id]?.url ?? "", title: snapshot[id]?.title ?? "" })), -- active: state.active -- } -- }; -- } -- writeSaved(); -- }; -- const ensureTabs = (botId) => { -- if (tabs.getSnapshot()[botId] !== void 0) return; -- const entries2 = saved[botId]?.tabs ?? []; -- const ids = entries2.map((entry) => entry.id); -- const active = ids.includes(saved[botId]?.active) ? saved[botId].active : ids.at(-1) ?? null; -- for (const entry of entries2) owner.set(entry.id, botId); -- tabs.set((state) => ({ ...state, [botId]: { ids, active } })); -- for (const entry of entries2) publish2(entry.id, { url: entry.url ?? "", title: entry.title ?? "", live: false }); -- }; -- const ensureLayer = () => { -- if (layer?.isConnected) return layer; -- layer = doc.createElement("div"); -- layer.className = "bt-browser-layer"; -- Object.assign(layer.style, { position: "fixed", inset: "0", zIndex: "41", pointerEvents: "none" }); -- doc.body.append(layer); -- return layer; -- }; -- const place = (entry) => { -- const box = rect ?? { left: 0, top: 0, width: 800, height: 600 }; -- Object.assign(entry.element.style, { -- left: `${box.left}px`, -- top: `${box.top}px`, -- width: `${box.width}px`, -- height: `${box.height}px`, -- visibility: shown === entry.tabId && visible && rect ? "visible" : "hidden" -- }); -- }; -- async function preview(tabId) { -- const entry = views.get(tabId); -- if (entry === void 0 || !entry.ready || !/^https?:/i.test(entry.element.getURL())) return false; -- let timer; -- const timeout = new Promise((resolve) => { -- timer = setTimeout(() => resolve(""), PREVIEW_TIMEOUT_MS); -- }); -+ for (const key of LEGACY_BROWSER_KEYS) { - try { -- const excerpt = await Promise.race([entry.element.executeJavaScript(PREVIEW_SCRIPT), timeout]); -- if (typeof excerpt !== "string" || excerpt === "" || views.get(tabId) !== entry) return false; -- publish2(tabId, { excerpt }); -- return true; -+ const value = storage.getItem(key); -+ if (value === null) continue; -+ const previous = JSON.parse(value) ?? {}; -+ const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => { -+ const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" }; -+ return [botId, { tabs: [tab], active: tab.id }]; -+ })); -+ storage.setItem(STORAGE.browsers, JSON.stringify(tabs)); -+ for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier); -+ return; - } catch { -- return false; -- } finally { -- clearTimeout(timer); - } - } -- const observe = (entry) => { -- const { element } = entry; -- if (!entry.ready) return; -- const url = element.getURL(); -- if (url.startsWith("about:blank#")) return; -- if (entry.fresh) { -- element.clearHistory(); -- entry.fresh = false; -- } -- const title = element.getTitle(); -- publish2(entry.tabId, { url, title, loading: element.isLoading(), canGoBack: element.canGoBack(), canGoForward: element.canGoForward(), live: true }); -- persist(entry.botId); -- }; -- async function create(tabId) { -- const botId = owner.get(tabId); -- const reservation = await bridge.acquire(`bot:${botId}`); -- if (disposed) { -- await bridge.release(reservation.lease).catch(() => { -- }); -- return void 0; -- } -- const element = doc.createElement("webview"); -- element.dataset.sidebarBrowserFrame = "webview"; -- Object.assign(element.style, { position: "fixed", display: "flex", border: "0", pointerEvents: "auto", background: "#fff" }); -- element.dataset.btBrowser = tabId; -- element.setAttribute("name", reservation.lease); -- element.setAttribute("partition", reservation.partition); -- element.setAttribute("allowpopups", ""); -- element.setAttribute("src", `about:blank#${reservation.lease}`); -- const entry = { botId, tabId, element, lease: reservation.lease, ready: false, fresh: true, pending: pages.getSnapshot()[tabId]?.url || savedTab(botId, tabId)?.url, usedAt: Date.now() }; -- const lifetime = new AbortController(); -- entry.stop = () => lifetime.abort(); -- const { signal } = lifetime; -- const unsubscribe = bridge.onOpenRequested(reservation.lease, (url) => void openTab(botId, url)); -- signal.addEventListener("abort", unsubscribe, { once: true }); -- element.addEventListener("dom-ready", () => { -- entry.ready = true; -- observe(entry); -- if (entry.pending) { -- const url = entry.pending; -- entry.pending = void 0; -- void element.loadURL(url).catch(() => { -- }); -+}; -+function migrateStorage(storage = globalThis.localStorage) { -+ for (const key of Object.values(STORAGE)) { -+ if (key === STORAGE.browsers) continue; -+ for (const legacy of legacyKeys(key)) { -+ try { -+ const value = storage.getItem(legacy); -+ if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); -+ storage.removeItem(legacy); -+ } catch { - } -- }, { signal }); -- for (const name of ["did-navigate", "did-navigate-in-page", "did-start-loading", "did-stop-loading", "page-title-updated"]) { -- element.addEventListener(name, () => observe(entry), { signal }); - } -- element.addEventListener("did-stop-loading", () => { -- void preview(tabId); -- }, { signal }); -- element.addEventListener("page-favicon-updated", (event) => { -- const favicon = event.favicons?.[0]; -- if (typeof favicon === "string" && favicon.startsWith("data:")) publish2(tabId, { favicon }); -- }, { signal }); -- element.addEventListener("did-start-navigation", (event) => { -- if (!event.isMainFrame) return; -- if (blockedAddress(event.url)) { -- element.stop(); -- publish2(tabId, { loading: false, error: BLOCKED }); -- return; -- } -- publish2(tabId, { loading: true, error: null }); -- }, { signal }); -- element.addEventListener("did-fail-load", (event) => { -- if (event.isMainFrame && event.errorCode !== -3) publish2(tabId, { loading: false, error: event.errorDescription || `Error ${event.errorCode}` }); -- }, { signal }); -- element.addEventListener("render-process-gone", () => { -- if (views.get(tabId) === void 0) return; -- void release(tabId); -- publish2(tabId, { loading: false, error: "The page crashed." }); -- }, { signal }); -- views.set(tabId, entry); -- ensureLayer().append(element); -- place(entry); -- publish2(tabId, { live: true, loading: Boolean(entry.pending) }); -- return entry; - } -- const opening = /* @__PURE__ */ new Map(); -- function open(tabId) { -- const entry = views.get(tabId); -- if (entry) return Promise.resolve(entry); -- let job = opening.get(tabId); -- if (job === void 0) { -- job = create(tabId).catch((error) => { -- console.warn("[ds-bot] browser acquire", error); -- publish2(tabId, { loading: false, error: String(error?.message ?? error) }); -- return void 0; -- }).finally(() => opening.delete(tabId)); -- opening.set(tabId, job); -- void job.then(() => trim()); -+ try { -+ migrateBrowsers(storage); -+ } catch { -+ } -+ try { -+ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -+ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -+ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); - } -- return job; -+ } catch { - } -- function trim() { -- const idle = [...views.values()].filter((entry) => entry.tabId !== shown).sort((a, b) => a.usedAt - b.usedAt); -- while (views.size > MAX_LIVE && idle.length > 0) void release(idle.shift().tabId); -+} -+ -+// src/client/details-drawer.js -+var import_react44 = require("react"); -+var TAB_LABELS = { details: "Details", browser: "Browser" }; -+var hostOf = (url) => { -+ try { -+ return new URL(url).hostname.replace(/^www\./, ""); -+ } catch { -+ return ""; - } -- async function release(tabId) { -- const entry = views.get(tabId); -- if (entry === void 0) return; -- views.delete(tabId); -- entry.stop(); -- entry.element.remove(); -- publish2(tabId, { live: false, loading: false }); -- await bridge.release(entry.lease).catch((error) => console.warn("[ds-bot] browser release", error)); -+}; -+var shortUrl = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, ""); -+var EMPTY_TABS = { ids: [], active: null }; -+var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; -+var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); -+function PermissionRow({ bot, roster, actions, run }) { -+ const [pending, setPending] = (0, import_react44.useState)(null); -+ const catalog = roster.permissions; -+ const saved = bot.permission ?? catalog?.defaultPreset; -+ (0, import_react44.useEffect)(() => { -+ if (pending === saved) setPending(null); -+ }, [pending, saved]); -+ if (!catalog?.options?.length) return null; -+ const current = pending ?? saved; -+ const pick2 = (value) => { -+ if (value === current && bot.permission !== void 0) return; -+ setPending(value); -+ void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); -+ }; -+ return (0, import_react44.createElement)( -+ "div", -+ { className: "bt-drawer-permission" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Permissions")), -+ (0, import_react44.createElement)( -+ Segmented, -+ { label: t("Permissions"), value: current }, -+ catalog.options.map((option) => (0, import_react44.createElement)("button", { -+ key: option.value, -+ type: "button", -+ "aria-pressed": option.value === current, -+ title: option.description, -+ onClick: () => pick2(option.value), -+ className: DANGER.has(option.value) ? "bt-perm-danger" : void 0 -+ }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name)) -+ ) -+ ); -+} -+function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { -+ const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); -+ const pages = useBrowserPages((map) => map); -+ (0, import_react44.useEffect)(() => { -+ actions.browserEnsure?.(bot.id); -+ }, [bot.id]); -+ if (roster.browser !== true || !actions.browserShow) { -+ return (0, import_react44.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); - } -- function openTab(botId, url) { -- ensureTabs(botId); -- const tabId = `t${Date.now().toString(36)}-${++serial}`; -- owner.set(tabId, botId); -- tabs.set((state) => { -- const own = state[botId] ?? { ids: [], active: null }; -- return { ...state, [botId]: { ids: [...own.ids, tabId], active: tabId } }; -- }); -- publish2(tabId, { url: "", title: "", live: false }); -- if (typeof url === "string" && url !== "") void navigate(tabId, url); -- persist(botId); -- return tabId; -+ if (state.ids.length === 0) { -+ return (0, import_react44.createElement)( -+ "div", -+ { className: "bt-browser-empty-mark" }, -+ (0, import_react44.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), -+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react44.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) -+ ); - } -- async function closeTab(botId, tabId) { -- const state = tabs.getSnapshot()[botId]; -- if (state === void 0 || !state.ids.includes(tabId)) return; -- await opening.get(tabId)?.catch(() => { -- }); -- if (shown === tabId) await show(null); -- await release(tabId); -- tabs.set((snapshot) => { -- const current = snapshot[botId]; -- if (current === void 0 || !current.ids.includes(tabId)) return snapshot; -- const at = current.ids.indexOf(tabId); -- const ids = current.ids.filter((id) => id !== tabId); -- const active = current.active === tabId ? ids[Math.min(at, ids.length - 1)] ?? null : current.active; -- const next = { ...snapshot }; -- if (ids.length === 0) delete next[botId]; -- else next[botId] = { ids, active }; -- return next; -- }); -- owner.delete(tabId); -- pages.set((snapshot) => { -- if (snapshot[tabId] === void 0) return snapshot; -- const { [tabId]: _, ...rest } = snapshot; -- return rest; -+ return (0, import_react44.createElement)( -+ "div", -+ { className: "bt-bcards" }, -+ state.ids.map((tabId, index) => { -+ const page = pages[tabId] ?? {}; -+ const label = page.title || shortUrl(page.url) || t("New tab"); -+ return (0, import_react44.createElement)( -+ "div", -+ { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, -+ (0, import_react44.createElement)( -+ "button", -+ { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, -+ page.excerpt ? (0, import_react44.createElement)( -+ "span", -+ { className: "bt-bcard-preview" }, -+ (0, import_react44.createElement)("span", { className: "bt-bcard-ptitle" }, label), -+ (0, import_react44.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) -+ ) : (0, import_react44.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react44.createElement)(GlobeIcon), (0, import_react44.createElement)("span", null, label)) -+ ), -+ (0, import_react44.createElement)("button", { -+ type: "button", -+ className: "bt-bcard-x", -+ "aria-label": t("Close tab"), -+ title: t("Close tab"), -+ onClick: () => { -+ void actions.browserCloseTab(bot.id, tabId); -+ } -+ }, "×"), -+ (0, import_react44.createElement)( -+ "div", -+ { className: "bt-bcard-row" }, -+ (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), -+ (0, import_react44.createElement)("span", { className: "bt-bcard-title" }, label), -+ page.url ? (0, import_react44.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null -+ ) -+ ); -+ }), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react44.createElement)(PlusIcon), t("New tab")) -+ ); -+} -+function About({ text: text2, title = "About" }) { -+ const [open, setOpen] = (0, import_react44.useState)(false); -+ const long = text2.split("\n").length > 5 || text2.length > 240; -+ const body = (0, import_react44.useRef)(null); -+ useMorph(body, open); -+ return (0, import_react44.createElement)( -+ "div", -+ null, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t(title)), -+ (0, import_react44.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), -+ long ? (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null -+ ); -+} -+function SharedRoomApprovals({ room: room2 }) { -+ const [items, setItems] = (0, import_react44.useState)(room2.pendingApprovals ?? []); -+ const [busy, setBusy] = (0, import_react44.useState)(""); -+ const face = room2.origin === "face-to-face"; -+ (0, import_react44.useEffect)(() => { -+ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; -+ if (haveItems) setItems(room2.pendingApprovals); -+ if (room2.viewerRole !== "invited-owner" && !face) return void 0; -+ let alive = true; -+ call("social/status", {}).then((value) => { -+ if (!alive) return; -+ if (room2.viewerRole === "invited-owner" && !haveItems) { -+ setItems(Array.isArray(value?.approvals) ? value.approvals : []); -+ } -+ }, () => { - }); -- persist(botId); -- } -- function activate(botId, tabId) { -- ensureTabs(botId); -- const state = tabs.getSnapshot()[botId]; -- if (state === void 0 || !state.ids.includes(tabId) || state.active === tabId) return; -- tabs.set((snapshot) => ({ ...snapshot, [botId]: { ...state, active: tabId } })); -- persist(botId); -- } -- const activeTab = (botId) => tabs.getSnapshot()[botId]?.active ?? null; -- async function show(tabId, box = null) { -- if (tabId !== null && tabId === shown && visible && box !== null) { -- rect = box; -- const entry2 = views.get(tabId); -- if (entry2) place(entry2); -- clearTimeout(resizeLater); -- resizeLater = setTimeout(() => { -- for (const other of views.values()) place(other); -- }, RESIZE_LATER_MS); -+ return () => { -+ alive = false; -+ }; -+ }, [room2.id, room2.viewerRole, room2.pendingCount, face]); -+ if (room2.viewerRole !== "invited-owner" && !face || !items.length) return null; -+ const decide = async (item, decision) => { -+ setBusy(item.id); -+ try { -+ await call("social/decide-approval", { approvalId: item.id, decision }); -+ setItems((list) => list.filter((entry) => entry.id !== item.id)); -+ } finally { -+ setBusy(""); -+ } -+ }; -+ return (0, import_react44.createElement)( -+ "div", -+ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react44.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), -+ items.map((item) => (0, import_react44.createElement)( -+ "div", -+ { key: item.id, className: "bt-set-row" }, -+ (0, import_react44.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) -+ )) -+ ); -+} -+function FaceMembers({ room: room2, roster, actions }) { -+ const people2 = facePeople(room2, roster); -+ const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); -+ return (0, import_react44.createElement)( -+ "div", -+ null, -+ (0, import_react44.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), -+ (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react44.createElement)(FacePersonRow, { -+ key: person.key, -+ person, -+ roster, -+ className: "bt-member", -+ onOpenBot: (id) => actions.openSession(id), -+ lead: room2.face?.lead?.handle -+ }))) -+ ); -+} -+function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) { -+ const live = room2.presentedParticipants ?? room2.participants ?? []; -+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0); -+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0); -+ const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -+ const { entries: entries2, now } = useMemberWork(room2, useWork); -+ const invited = room2.viewerRole === "invited-owner"; -+ const title = roomTitle(room2); -+ const leaveMine = () => { -+ const mine = remotes.find((p) => p.ownerId === roster.self?.memberId) || members.find((member) => member.ownerId === roster.self?.memberId); -+ if (mine) void actions.updateRoom(room2.id, { removeParticipants: [{ kind: "bot", id: mine.handle || mine.id, remote: Boolean(mine.handle) }] }); -+ }; -+ const face = room2.origin === "face-to-face"; -+ return [ -+ (0, import_react44.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), -+ invited && room2.sharedFrom && !face ? (0, import_react44.createElement)( -+ "div", -+ { key: "from", className: "bt-shared-from" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, title), -+ (0, import_react44.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), -+ room2.sharedFrom.invitedAt ? (0, import_react44.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) -+ ) : face ? null : room2.notice ? (0, import_react44.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react44.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), -+ face ? null : (0, import_react44.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), -+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react44.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react44.createElement)( -+ "div", -+ { key: "members" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), -+ (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, [ -+ ...humans.map((person) => (0, import_react44.createElement)( -+ "div", -+ { key: person.id, role: "listitem" }, -+ (0, import_react44.createElement)( -+ "div", -+ { className: "bt-member" }, -+ (0, import_react44.createElement)("span", { className: "bt-member-name" }, (0, import_react44.createElement)("span", null, person.displayName || person.id)), -+ (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Human")), -+ person.role === "admin" ? (0, import_react44.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -+ ) -+ )), -+ ...remotes.map((person) => (0, import_react44.createElement)( -+ "div", -+ { key: person.handle || person.id, role: "listitem" }, -+ (0, import_react44.createElement)( -+ "div", -+ { className: "bt-member" }, -+ (0, import_react44.createElement)( -+ "span", -+ { className: "bt-member-name" }, -+ (0, import_react44.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react44.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) -+ ), -+ face ? null : (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) -+ ) -+ )), -+ ...members.map((member) => { -+ const entry = entries2.find((item) => item.id === member.id); -+ return (0, import_react44.createElement)( -+ "div", -+ { key: member.id, role: "listitem" }, -+ (0, import_react44.createElement)( -+ "div", -+ { className: "bt-member" }, -+ (0, import_react44.createElement)( -+ "button", -+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, -+ (0, import_react44.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), -+ (0, import_react44.createElement)("span", null, member.name) -+ ), -+ room2.admin === member.id ? (0, import_react44.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -+ ), -+ entry ? (0, import_react44.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null -+ ); -+ }) -+ ]) -+ ), -+ face ? null : (0, import_react44.createElement)( -+ "div", -+ { key: "mode" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Who answers")), -+ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) -+ ), -+ face || !usage ? null : (0, import_react44.createElement)( -+ "div", -+ { key: "usage" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, room2.name), (0, import_react44.createElement)("span", null, usage)) -+ ) -+ ]; -+} -+function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { -+ const owner = roster.byId[sessionId]; -+ const part = owner ? currentPart(owner) : sessionId; -+ const isMainBot = isMainOf(roster, owner?.id); -+ const [tab, setTab] = (0, import_react44.useState)("details"); -+ const room2 = roster.roomsById[sessionId]; -+ const [view, setView] = (0, import_react44.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); -+ const [editing, setEditing] = (0, import_react44.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); -+ const [draftName, setDraftName] = (0, import_react44.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); -+ const [copied, setCopied] = (0, import_react44.useState)(false); -+ const [error, setError] = (0, import_react44.useState)(""); -+ const glide = useGlide(tab); -+ const last = (0, import_react44.useRef)({ view, tab, move: void 0 }); -+ if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; -+ else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; -+ const move = last.current.move; -+ const side = move === "tab" ? void 0 : move; -+ const close = () => actions.closeOverlay(); -+ useEscape(close); -+ const bot = owner; -+ const running = useSessionStatus((map) => map.get(part)?.running === true); -+ const doing = useActivity((map) => map[bot?.id ?? sessionId]); -+ const transcript = useSessions((list) => transcriptOf(list, part, bot?.name ?? room2?.name ?? t("Bot"), roster.exchangeTurns[part], copyNames(roster))); -+ const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); -+ const run = (promise) => Promise.resolve(promise).then(() => { -+ setError(""); -+ return true; -+ }).catch((failure) => { -+ setError(failure?.message ?? String(failure)); -+ return false; -+ }); -+ const share = () => { -+ const text2 = transcript; -+ if (!text2) { -+ actions.notify?.(t("Nothing to copy yet")); - return; - } -- shown = tabId; -- if (box) rect = box; -- visible = tabId !== null && box !== null; -- for (const entry2 of views.values()) place(entry2); -- if (tabId === null) return; -- const entry = await open(tabId); -- if (entry === void 0 || shown !== tabId) return; -- entry.usedAt = Date.now(); -- place(entry); -- } -- async function navigate(tabId, input) { -- const url = browserAddress(input); -- if (url === null || blockedAddress(url)) { -- publish2(tabId, { error: url === null ? "Only http and https addresses open here." : BLOCKED }); -- return false; -- } -- const entry = await open(tabId); -- if (entry === void 0) return false; -- publish2(tabId, { url, loading: true, error: null }); -- persist(owner.get(tabId)); -- if (!entry.ready) entry.pending = url; -- else void entry.element.loadURL(url).catch(() => { -+ void copiedToClipboard(text2).then((written) => { -+ if (!written) { -+ actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); -+ return; -+ } -+ setCopied(true); -+ setTimeout(() => setCopied(false), 1600); -+ actions.notify?.(t("Conversation copied as Markdown")); - }); -- return true; -+ }; -+ const errorLine = error ? (0, import_react44.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -+ if ((bot || room2) && view === "settings" && !bot?.remote) { -+ return [ -+ (0, import_react44.createElement)( -+ "div", -+ { key: "head", className: "bt-subhead", "data-move": move }, -+ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react44.createElement)(ChevronLeftIcon)), -+ (0, import_react44.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), -+ hasStrip ? null : (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react44.createElement)(CloseIcon)) -+ ), -+ (0, import_react44.createElement)( -+ "div", -+ { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, -+ errorLine, -+ bot ? (0, import_react44.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react44.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) -+ ) -+ ]; - } -- async function forget(botId) { -- ensureTabs(botId); -- for (const tabId of tabs.getSnapshot()[botId]?.ids ?? []) { -- await opening.get(tabId)?.catch(() => { -- }); -- if (shown === tabId) await show(null); -- await release(tabId); -- owner.delete(tabId); -- pages.set((snapshot) => { -- if (snapshot[tabId] === void 0) return snapshot; -- const { [tabId]: _, ...rest } = snapshot; -- return rest; -- }); -- } -- tabs.set((snapshot) => { -- if (snapshot[botId] === void 0) return snapshot; -- const { [botId]: _, ...rest } = snapshot; -- return rest; -- }); -- if (saved[botId] !== void 0) { -- const { [botId]: _, ...rest } = saved; -- saved = rest; -- writeSaved(); -- } -+ const top = (0, import_react44.createElement)( -+ "div", -+ { className: "bt-drawer-top", "data-move": side }, -+ bot || room2 ? (0, import_react44.createElement)("button", { -+ type: "button", -+ className: "bt-round", -+ "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -+ title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), -+ "data-info-row": "settings", -+ onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") -+ }, (0, import_react44.createElement)(GearIcon)) : null, -+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react44.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react44.createElement)(GlobeIcon)) : null, -+ bot || room2 ? (0, import_react44.createElement)("button", { -+ type: "button", -+ className: "bt-round", -+ // #1347: the accessible name says what the click does, which is copy — "Share conversation" -+ // promised a sharing step that never came. It still says "Copied" after a copy, so a screen -+ // reader hears the same confirmation the toast gives. -+ "aria-label": copied ? t("Copied conversation") : t("Copy conversation"), -+ title: copied ? t("Copied") : t("Copy conversation as Markdown"), -+ onClick: share -+ }, copied ? (0, import_react44.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react44.createElement)(ShareIcon)) : null, -+ hasStrip ? null : (0, import_react44.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react44.createElement)(CloseIcon)) -+ ); -+ if (!bot && !room2) { -+ return [ -+ (0, import_react44.createElement)(import_react44.Fragment, { key: "top" }, top), -+ (0, import_react44.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react44.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) -+ ]; - } -- return { -- tabs, -- pages, -- /** Restore a Bot's saved strip (or an empty one) so the panel can render it. */ -- ensureTabs, -- openTab, -- closeTab, -- activate, -- preview, -- show, -- navigate, -- command(tabId, name) { -- const entry = views.get(tabId); -- if (!entry?.ready) return; -- if (name === "back" && entry.element.canGoBack()) entry.element.goBack(); -- else if (name === "forward" && entry.element.canGoForward()) entry.element.goForward(); -- else if (name === "reload") entry.element.reload(); -- else if (name === "stop") entry.element.stop(); -- }, -- focus(tabId) { -- views.get(tabId)?.element.focus(); -- }, -- /** Pages a read can reach: each Bot's active tab, live and showing a document. */ -- openPages() { -- const open2 = {}; -- const snapshot = pages.getSnapshot(); -- for (const [botId, state] of Object.entries(tabs.getSnapshot())) { -- const tabId = state.active; -- const entry = tabId ? views.get(tabId) : void 0; -- const page = tabId ? snapshot[tabId] : void 0; -- if (entry?.ready && page?.url) open2[botId] = { url: page.url, title: page.title }; -- } -- return open2; -- }, -- /** A Bot's read reaches its active tab. */ -- async read(botId) { -- const tabId = activeTab(botId); -- const entry = tabId ? views.get(tabId) : void 0; -- if (!entry?.ready) throw new Error("This Bot has no browser page open."); -- let timer; -- const timeout = new Promise((_, reject) => { -- timer = setTimeout(() => reject(new Error("The page took too long to read.")), READ_TIMEOUT_MS); -- }); -- try { -- return await Promise.race([entry.element.executeJavaScript(READ_PAGE_SCRIPT), timeout]); -- } finally { -- clearTimeout(timer); -- } -- }, -- noteRead(botId) { -- const tabId = activeTab(botId); -- if (tabId) publish2(tabId, { readAt: Date.now() }); -- }, -- release, -- forget, -- /** Forget every Bot outside `botIds`, as when Bots were deleted from another window. */ -- retain(botIds) { -- const keep = new Set(botIds); -- const known = /* @__PURE__ */ new Set([...Object.keys(tabs.getSnapshot()), ...Object.keys(saved), ...owner.values()]); -- for (const entry of views.values()) known.add(entry.botId); -- return Promise.all([...known].filter((botId) => !keep.has(botId)).map(forget)); -- }, -- dispose() { -- disposed = true; -- clearTimeout(resizeLater); -- for (const entry of views.values()) void release(entry.tabId); -- layer?.remove(); -- } -+ const current = bot?.name ?? room2.name; -+ const saveName = async () => { -+ setEditing(false); -+ const next = draftName.trim(); -+ if (next === current || room2 && !room2.named && next === "") return; -+ if (bot && next) await run(actions.updateBot(bot.id, { name: next })); -+ if (room2) await run(actions.updateRoom(room2.id, { name: next })); - }; -+ const subtitle = bot ? bot.role : roomSubtitle(room2, roster); -+ const tabs = bot ? ["details", "browser"] : ["details"]; -+ return [ -+ (0, import_react44.createElement)(import_react44.Fragment, { key: "top" }, top), -+ (0, import_react44.createElement)( -+ "div", -+ { key: "id", className: "bt-drawer-id", "data-move": side }, -+ bot ? (0, import_react44.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react44.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ bot?.remote ? (0, import_react44.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react44.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react44.createElement)("input", { -+ className: "bt-drawer-name-input", -+ autoFocus: true, -+ value: draftName, -+ "aria-label": bot ? t("Bot name") : t("Group name"), -+ placeholder: room2 ? t("Name after members") : void 0, -+ onChange: (event) => setDraftName(event.target.value), -+ onBlur: saveName, -+ onKeyDown: (event) => { -+ if (event.key === "Enter") void saveName(); -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ setEditing(false); -+ } -+ } -+ }) : (0, import_react44.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { -+ setDraftName(room2 && !room2.named ? "" : current); -+ setEditing(true); -+ } }, current), -+ subtitle ? (0, import_react44.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react44.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null -+ ), -+ bot?.remote ? null : (0, import_react44.createElement)( -+ "div", -+ { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, -+ (0, import_react44.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ tabs.map((key) => (0, import_react44.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) -+ ), -+ (0, import_react44.createElement)( -+ "div", -+ { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, -+ errorLine, -+ room2 && tab === "details" ? (0, import_react44.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, -+ bot && bot.remote && tab === "details" ? [ -+ bot.remote.handle ? (0, import_react44.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, -+ (0, import_react44.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) -+ ] : null, -+ bot && !bot.remote && tab === "details" ? [ -+ (0, import_react44.createElement)( -+ "div", -+ { key: "model", className: "bt-drawer-model" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Model")), -+ (0, import_react44.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), -+ modelOf(bot, roster) ? null : (0, import_react44.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) -+ ), -+ (0, import_react44.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), -+ (0, import_react44.createElement)( -+ "div", -+ { key: "folder", className: "bt-drawer-model" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Working folder")), -+ (0, import_react44.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) -+ ), -+ bot.instructions ? (0, import_react44.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react44.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), -+ roster.memory === false ? null : (0, import_react44.createElement)(MemoryRow, { key: "memory", bot, actions }), -+ usage ? (0, import_react44.createElement)( -+ "div", -+ { key: "usage" }, -+ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react44.createElement)("div", { className: "bt-kv" }, (0, import_react44.createElement)("span", null, bot.name), (0, import_react44.createElement)("span", null, usage)) -+ ) : null, -+ (0, import_react44.createElement)(ScheduleSummary, { key: "routines", bot, actions }) -+ ] : null, -+ bot && !bot.remote && tab === "browser" ? (0, import_react44.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null -+ ) -+ ]; -+} -+ -+// src/client/browser-pane.js -+var import_react45 = require("react"); -+var import_react_dom10 = require("react-dom"); -+var PANEL_LINGER_MS = 300; -+var WIDTH_KEY = STORAGE.browserWidth; -+var READ_FLASH_MS = 2400; -+var EMPTY_PAGE = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, excerpt: "" }; -+var EMPTY_TABS2 = { ids: [], active: null }; -+var SPRING_LEAD2 = springEasing(576, 39.4); -+var SPRING_TRAIL2 = springEasing(144, 19.7); -+var icon2 = (paths) => function BrowserIcon() { -+ return (0, import_react45.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 }, -+ paths.map((d, index) => (0, import_react45.createElement)("path", { key: index, d })) -+ ); -+}; -+var GlobeIcon = icon2(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); -+var BackIcon = icon2(["M12.5 4.5L7 10l5.5 5.5"]); -+var ForwardIcon = icon2(["M7.5 4.5L13 10l-5.5 5.5"]); -+var ReloadIcon = icon2(["M15.5 10a5.5 5.5 0 1 1-1.6-3.9", "M15.5 3.5v3.5H12"]); -+var StopIcon = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); -+var CloseIcon2 = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); -+var PlusTabIcon = icon2(["M10 5v10M5 10h10"]); -+var LockIcon2 = icon2(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); -+function Favicon({ src, className }) { -+ const [failed, setFailed] = (0, import_react45.useState)(null); -+ if (!src || failed === src) return (0, import_react45.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react45.createElement)(GlobeIcon)); -+ return (0, import_react45.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); - } --var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); --function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.randomUUID(), retryMs = 1e3 }) { -- let stopped = false; -- let poll; -- let reported = ""; -- const answered = /* @__PURE__ */ new Map(); -- const answer = async ({ id, botId }) => { -- let job = answered.get(id); -- if (job === void 0) { -- job = (async () => { -- try { -- const result = { id, ok: true, page: await browsers.read(botId) }; -- browsers.noteRead(botId); -- return result; -- } catch (error) { -- return { id, ok: false, error: error instanceof Error ? error.message : String(error) }; -- } -- })(); -- answered.set(id, job); -- if (answered.size > 200) answered.delete(answered.keys().next().value); -+var savedWidth = () => { -+ try { -+ const value = Number(localStorage.getItem(WIDTH_KEY)); -+ return Number.isFinite(value) && value >= MIN_DETAILS ? value : null; -+ } catch { -+ return null; -+ } -+}; -+var fitWidth = (width, [low, high]) => Math.round(Math.max(low, Math.min(width, high))); -+var widthRange = (view, viewport) => panelWidthRange(view, viewport); -+var room = { rule: null, selector: "" }; -+var roomRule = () => { -+ const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -+ if (live && room.selector) return room.rule; -+ const column = document.querySelector('[class*="_centerCol"]'); -+ const selector = column ? [...column.classList].filter((name) => name.includes("_centerCol")).map((name) => `.${CSS.escape(name)}`).join(",") : ""; -+ if (live && !selector) return room.rule; -+ const declarations2 = room.rule?.style.cssText ?? ""; -+ room.rule?.parentStyleSheet?.ownerNode?.remove(); -+ const style = document.createElement("style"); -+ style.dataset.dshBot = "chat-room"; -+ style.textContent = `${selector || '[class*="_centerCol"]'}{${declarations2}}`; -+ document.head.append(style); -+ room.rule = style.sheet.cssRules[0]; -+ room.selector = selector; -+ return room.rule; -+}; -+var roomHold = 0; -+var chatRoom = { -+ /** The column's right margin in px, or null for the shell's own. */ -+ set(width) { -+ const { style } = roomRule(); -+ if (width === null) style.removeProperty("margin-right"); -+ else style.setProperty("margin-right", `${width}px`, "important"); -+ }, -+ /** While on, a margin change lands at once instead of gliding. */ -+ hold(on) { -+ cancelAnimationFrame(roomHold); -+ if (on) { -+ roomRule().style.setProperty("transition", "none", "important"); -+ return; - } -- await post("browser-result", await job).catch((error) => console.warn("[ds-bot] browser result", error)); -- }; -- const loop = async () => { -- let failures = 0; -- while (!stopped) { -- poll = new AbortController(); -- const open = browsers.openPages(); -- reported = JSON.stringify(open); -- try { -- const requests = await post("browser-wait", { clientId, open }, poll.signal); -- failures = 0; -- for (const request of Array.isArray(requests) ? requests : []) void answer(request); -- } catch { -- if (stopped) return; -- if (poll.signal.aborted) continue; -- failures += 1; -- await sleep(Math.min(3e4, retryMs * 2 ** Math.min(failures, 5))); -- } -+ roomHold = requestAnimationFrame(() => { -+ roomHold = requestAnimationFrame(() => roomRule().style.removeProperty("transition")); -+ }); -+ }, -+ /** The columns themselves, for a drag's per-frame writes. */ -+ columns() { -+ roomRule(); -+ return room.selector ? [...document.querySelectorAll(room.selector)] : []; -+ } -+}; -+var shortAddress = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, ""); -+var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; -+var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); -+function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { -+ const pages = useBrowserPages((map) => map); -+ const wrap = (0, import_react45.useRef)(null); -+ const ind = (0, import_react45.useRef)(null); -+ const spot = (0, import_react45.useRef)(null); -+ const under = (0, import_react45.useRef)(null); -+ (0, import_react45.useLayoutEffect)(() => { -+ const box = wrap.current; -+ const capsule = ind.current; -+ if (!box || !capsule) return void 0; -+ const node = view === "details" ? box.querySelector(".bt-ptab-bot") : box.querySelector(`[data-tab="${active}"]`); -+ under.current = node; -+ if (!node) { -+ capsule.style.opacity = "0"; -+ spot.current = null; -+ return void 0; - } -- }; -- const offPages = browsers.pages.subscribe(() => { -- if (JSON.stringify(browsers.openPages()) !== reported) poll?.abort(); -+ if (view === "details") box.scrollLeft = 0; -+ else node.scrollIntoView?.({ block: "nearest", inline: "nearest" }); -+ const to = { left: node.offsetLeft, right: box.offsetWidth - node.offsetLeft - node.offsetWidth }; -+ const from = spot.current; -+ spot.current = to; -+ capsule.style.opacity = "1"; -+ capsule.style.left = `${to.left}px`; -+ capsule.style.right = `${to.right}px`; -+ if (from === null || from.left === to.left && from.right === to.right || reducedMotion2()) return void 0; -+ const ahead = to.left > from.left ? "right" : "left"; -+ const behind = ahead === "right" ? "left" : "right"; -+ const lead = capsule.animate([{ [ahead]: `${from[ahead]}px` }, { [ahead]: `${to[ahead]}px` }], { duration: SPRING_LEAD2.duration, easing: SPRING_LEAD2.easing }); -+ const trail = capsule.animate([{ [behind]: `${from[behind]}px` }, { [behind]: `${to[behind]}px` }], { duration: SPRING_TRAIL2.duration, easing: SPRING_TRAIL2.easing }); -+ return () => { -+ lead.cancel(); -+ trail.cancel(); -+ }; - }); -- void loop(); -- return () => { -- stopped = true; -- offPages(); -- poll?.abort(); -- }; --} -- --// src/client/mobile.js --var MOBILE_MAX_WIDTH = 640; --var MOBILE_CSS = ` --@media (max-width:${MOBILE_MAX_WIDTH}px){ --.bt-held-actions{max-width:100%} --.bt-held-card{padding:10px 12px} --:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px} --:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer} --:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} --:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default} --.bt-held-retry{min-height:32px} --.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))} --.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)} --.bt-flybot-pointer{max-width:100%;min-width:0} --.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none} --.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} --.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px} --.bt-inbox-card{flex-wrap:wrap;row-gap:8px} --.bt-inbox-copy{flex-basis:calc(100% - 38px)} --/* In progress and Done only. A Needs you ask is the whole text: inbox.js unsets this clamp on those five statuses (#1177). */ --.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2} --.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px} --.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px} --} --`; -- --// src/client/panes.js --var PANE_MIN_WIDTH_CSS = ` --.bt-panel{min-width:0} --.bt-panel-view{min-width:0} --.bt-drawer-body{min-width:0;overflow-wrap:anywhere} --.bt-drawer-body :is(.bt-muted,.bt-note,.bt-section-title,.bt-drawer-role,.bt-drawer-permission){min-width:0;overflow-wrap:anywhere} --.bt-panel :is(.bt-member,.bt-members,.bt-bcard-row,.bt-drawer-model,.bt-usage-row){min-width:0} --.bt-panel :is(.bt-muted,.bt-note){white-space:normal} --[class*="_centerCol"]{min-width:0} --html,body,#root{overflow-x:clip} --[class*="_frame"]>[class*="_centerCol"]{overflow-x:clip} --[class*="_frame"]:has(> [class*="_centerCol"]){overflow-x:clip} --`; --var COMPOSER = "[data-composer-input], [data-composer-card]"; --function clipScroll(el) { -- if (el && el.scrollLeft > 0) el.scrollLeft = 0; --} --function appFrameOf(doc) { -- if (typeof doc.querySelector !== "function") return null; -- const col = doc.querySelector('[class*="_centerCol"]'); -- const frame = col?.parentElement; -- if (!frame) return null; -- const name = frame.className; -- const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; -- return text2.includes("_frame") ? frame : null; --} --function levelShell(doc, from) { -- const page = doc.scrollingElement || doc.documentElement; -- clipScroll(page); -- clipScroll(doc.documentElement); -- clipScroll(doc.body); -- clipScroll(appFrameOf(doc)); -- for (let at = from?.parentElement || null; at; at = at.parentElement) clipScroll(at); --} --function composerOf(node) { -- let el = node; -- if (el && el.nodeType !== 1) el = el.parentElement; -- if (!el || typeof el.closest !== "function") return null; -- return el.closest(COMPOSER); -+ (0, import_react45.useEffect)(() => { -+ const box = wrap.current; -+ if (!box) return void 0; -+ const observer = new ResizeObserver(() => { -+ const node = under.current; -+ const capsule = ind.current; -+ if (!node?.isConnected || !capsule || spot.current === null) return; -+ spot.current = { left: node.offsetLeft, right: box.offsetWidth - node.offsetLeft - node.offsetWidth }; -+ capsule.style.left = `${spot.current.left}px`; -+ capsule.style.right = `${spot.current.right}px`; -+ }); -+ observer.observe(box); -+ return () => observer.disconnect(); -+ }, []); -+ return (0, import_react45.createElement)( -+ "div", -+ { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, -+ (0, import_react45.createElement)( -+ "div", -+ { className: "bt-ptabs-in", ref: wrap }, -+ (0, import_react45.createElement)( -+ "button", -+ { -+ type: "button", -+ role: "tab", -+ "aria-selected": view === "details", -+ className: "bt-ptab bt-ptab-bot", -+ title: bot.name, -+ onClick: () => onView("details") -+ }, -+ (0, import_react45.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), -+ view === "details" ? (0, import_react45.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null -+ ), -+ ids.map((tabId) => { -+ const page = pages[tabId] ?? EMPTY_PAGE; -+ const on = view === "browser" && tabId === active; -+ return (0, import_react45.createElement)( -+ "button", -+ { -+ key: tabId, -+ type: "button", -+ role: "tab", -+ "aria-selected": on, -+ "data-tab": tabId, -+ className: "bt-ptab", -+ title: page.url || t("New tab"), -+ onClick: () => { -+ actions.browserActivate(bot.id, tabId); -+ onView("browser"); -+ }, -+ onAuxClick: (event) => { -+ if (event.button === 1) { -+ event.preventDefault(); -+ void actions.browserCloseTab(bot.id, tabId); -+ } -+ } -+ }, -+ (0, import_react45.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), -+ (0, import_react45.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), -+ (0, import_react45.createElement)("span", { -+ className: "bt-ptab-x", -+ role: "button", -+ "aria-label": t("Close tab"), -+ title: t("Close tab"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ void actions.browserCloseTab(bot.id, tabId); -+ } -+ }, "×") -+ ); -+ }), -+ (0, import_react45.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) -+ ), -+ // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. -+ (0, import_react45.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react45.createElement)(PlusTabIcon)), -+ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react45.createElement)(CloseIcon2)) -+ ); - } --function watchShellLevel(doc) { -- const later = (from) => { -- const reset = () => levelShell(doc, from); -- reset(); -- if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); -- }; -- const onFocus = (event) => later(composerOf(event.target) || void 0); -- const onInput = (event) => { -- const composer = composerOf(event.target); -- if (composer) later(composer); -- }; -- const onScroll = (event) => { -- const target = event.target; -- if (target === doc || target === doc.documentElement || target === doc.body) levelShell(doc); -- }; -- doc.addEventListener("focusin", onFocus); -- doc.addEventListener("input", onInput); -- doc.addEventListener("scroll", onScroll, true); -- return () => { -- doc.removeEventListener("focusin", onFocus); -- doc.removeEventListener("input", onInput); -- doc.removeEventListener("scroll", onScroll, true); -+function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { -+ const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE; -+ const frame = (0, import_react45.useRef)(null); -+ const input = (0, import_react45.useRef)(null); -+ const [draft, setDraft] = (0, import_react45.useState)(null); -+ const showGuest = landed && !leaving && Boolean(page.url) && !page.error; -+ (0, import_react45.useLayoutEffect)(() => { -+ const element = frame.current; -+ if (!element || leaving || !tabId) { -+ void actions.browserShow(null); -+ return void 0; -+ } -+ let raf = 0; -+ const measure = () => { -+ cancelAnimationFrame(raf); -+ raf = 0; -+ const box = element.getBoundingClientRect(); -+ void actions.browserShow(tabId, showGuest ? { left: box.left, top: box.top, width: box.width, height: box.height } : null); -+ }; -+ const schedule = () => { -+ if (!raf) raf = requestAnimationFrame(measure); -+ }; -+ measure(); -+ const observer = new ResizeObserver(measure); -+ observer.observe(element); -+ window.addEventListener("resize", schedule); -+ return () => { -+ cancelAnimationFrame(raf); -+ observer.disconnect(); -+ window.removeEventListener("resize", schedule); -+ }; -+ }, [tabId, showGuest, leaving]); -+ (0, import_react45.useEffect)(() => { -+ setDraft(null); -+ if (landed && !page.url) input.current?.focus(); -+ }, [tabId, landed]); -+ const go = (event) => { -+ event.preventDefault(); -+ const text2 = draft ?? ""; -+ if (text2.trim() === "") return; -+ setDraft(null); -+ input.current?.blur(); -+ void actions.browserNavigate(tabId, text2).then((opened) => { -+ if (opened) actions.browserFocus(tabId); -+ }); - }; --} -- --// src/client/project-board.js --var import_react47 = require("react"); --var PROJECT_BOARD_CSS = ` --/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the -- bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the -- Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */ --.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))} --.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto} --.bt-project-board{padding:16px 18px} --.bt-project-group{margin:0 0 14px} --.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} --.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} --.bt-project-row[data-column="done"]{opacity:.7} --.bt-project-title-wrap{display:flex;flex-direction:column;gap:2px;min-width:0} --.bt-project-audit{font-size:11px;line-height:1.45;white-space:pre-wrap;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} --.bt-project-empty{padding:22px 4px;opacity:.7} --/* #1767: the record beside the cards. The same surface as a row, one step quieter, and no new -- component language: a heading, a list, and a monospace hash the reader can compare. */ --.bt-project-record{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} --.bt-project-record h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} --.bt-project-record h5{margin:10px 0 4px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;opacity:.55} --.bt-project-decision,.bt-project-meeting{display:flex;gap:10px;align-items:baseline;padding:6px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} --.bt-project-decision .bt-project-date,.bt-project-meeting .bt-project-date{font-size:11px;opacity:.6;min-width:76px;font-variant-numeric:tabular-nums} --.bt-project-decision .bt-project-hash{margin-left:auto;font-size:11px;opacity:.55;font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-decision .bt-project-title,.bt-project-meeting .bt-project-title{min-width:0} --.bt-project-path{font-size:11px;opacity:.6;font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-missing{font-size:11px;opacity:.6} --.bt-project-status{font-size:11px;text-transform:uppercase;letter-spacing:.04em;opacity:.6;margin-left:auto} --/* #1831: the card's level, the people it is addressed to, and the identity a row or a comment -- carries. A severe card is the only one painted louder, because it is the only one that waits. */ --.bt-project-level{font-size:10px;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px;border:1px solid var(--bt-line,rgba(127,127,127,.32));opacity:.75} --.bt-project-level[data-level="severe"]{opacity:1;border-color:#c2410c;color:#c2410c} --.bt-project-recipients{font-size:11px;opacity:.65} --.bt-project-paused{font-size:11px;opacity:.75;color:#c2410c} --.bt-project-holder{font-size:11px;opacity:.7} --.bt-project-speaks-for{font-size:11px;opacity:.7;font-style:italic} --.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} --.bt-project-metrics{margin:0 0 14px} --.bt-project-metric-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:8px} --.bt-project-metric{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border:1px solid rgba(127,127,127,.24);border-radius:8px} --.bt-project-metric-value{font-size:18px;font-weight:600} --.bt-project-metric-label{font-size:12px;font-weight:600} --.bt-project-metric-caption{font-size:10px;opacity:.6;line-height:1.3} --.bt-project-metrics-note{margin:0 0 6px;font-size:11px;opacity:.7} --.bt-project-running{margin:0 0 14px} --.bt-project-running-head,.bt-project-running-row{display:grid;grid-template-columns:2fr 1fr 1fr .8fr .8fr 1.2fr;gap:8px;font-size:12px;padding:3px 0} --.bt-project-running-head{opacity:.6;font-size:10px;letter-spacing:.03em} --.bt-project-running-row[role="button"]{cursor:pointer} --.bt-project-running-row[data-overdue="true"]{color:#c2410c} --.bt-project-counts{display:flex;gap:14px;align-items:center;margin:0 0 12px;font-size:12px;letter-spacing:.03em;opacity:.8} --.bt-project-counts b{font-weight:600} --.bt-project-group[data-queue="waiting"] .bt-project-row{cursor:default} --.bt-project-role{font-size:11px;opacity:.65;font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-wait{font-size:11px;opacity:.65;margin-left:auto} --.bt-project-state{font-size:11px;margin-left:auto} --.bt-project-row[data-state="stalled"]{border-left:2px solid #d9822b} --.bt-project-reason{font-size:11px;opacity:.7} -- --/* #1855: the finished cards are one group of their own, closed until the reader opens it — a -- Project with months of history reads as the work in flight, not as its archive. */ --.bt-project-done{margin:18px 0 0;border-top:1px solid var(--bt-line,rgba(127,127,127,.22));padding:12px 0 0} --.bt-project-done>summary{cursor:pointer;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7;list-style:none} --.bt-project-done>summary::-webkit-details-marker{display:none} --.bt-project-done>summary::before{content:'\\25B8';display:inline-block;width:12px;opacity:.6} --.bt-project-done[open]>summary::before{content:'\\25BE'} --.bt-project-done[open]>summary{margin:0 0 8px} -- --/* #1749: management panels. Same surface as the board, one form row per control, and a status -- line that quotes the host's own answer — a refused save reads where the reader pressed it. */ --.bt-project-panel{padding:16px 18px;display:flex;flex-direction:column;gap:12px} --.bt-project-panel h3{margin:0} --.bt-project-field{display:flex;flex-direction:column;gap:4px} --.bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} --.bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} --.bt-project-field textarea{min-height:88px;resize:vertical} --.bt-project-row-actions{display:flex;gap:8px;align-items:center} --.bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} --.bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} --.bt-project-member .bt-project-member-kind{margin-left:auto;font-size:11px;opacity:.6} --.bt-project-status-line{font-size:12px;opacity:.75;min-height:16px} --.bt-project-note{font-size:12px;opacity:.65;margin:0} -- --/* #1860: the Project team — who runs the Project, who speaks for the reader, and the role pool. -- One quiet block above the cards, and a Bot row is a button because a reader is looking a -- person up. The pool row keeps the rank it plays and the ranks the host said it may switch to. */ --.bt-project-team{margin:0 0 14px} --.bt-project-team h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} --.bt-project-team-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px} --.bt-project-team-row[data-open-bot]{cursor:pointer} --.bt-project-team-row[data-open-bot]:hover{background:var(--bt-hover,rgba(127,127,127,.14))} --.bt-project-team-label{font-size:12px;opacity:.7;min-width:96px} --.bt-project-team-bot{font-size:12px;font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-team-roles{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;opacity:.75} --.bt-project-team-role{font-family:var(--bt-mono,ui-monospace,monospace)} --.bt-project-team-myrole{margin-left:auto;font-size:11px;opacity:.65} --`; --var PROJECT_SOURCES = ["bot", "group", "workflow"]; --var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; --var text = (value) => typeof value === "string" ? value.trim() : ""; --var DONE_COLUMN = "done"; --var boardOfRow = (row, task) => { -- if (task === void 0) return null; -- if (row.source === "group") { -- const id = text(task.origin?.roomId); -- return id === "" ? null : { kind: "group", id }; -- } -- if (row.source === "bot") { -- const id = text(task.ownerBotId); -- return id === "" ? null : { kind: "bot", id }; -+ const editing = draft !== null; -+ const secure = /^https:/i.test(page.url); -+ return (0, import_react45.createElement)( -+ "div", -+ { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, -+ (0, import_react45.createElement)( -+ "div", -+ { className: "bt-browser-bar" }, -+ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react45.createElement)(BackIcon)), -+ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react45.createElement)(ForwardIcon)), -+ (0, import_react45.createElement)("button", { -+ type: "button", -+ className: "bt-icon-btn", -+ disabled: !page.url, -+ "aria-label": page.loading ? t("Stop") : t("Reload"), -+ title: page.loading ? t("Stop") : t("Reload"), -+ onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") -+ }, (0, import_react45.createElement)(page.loading ? StopIcon : ReloadIcon)), -+ (0, import_react45.createElement)( -+ "form", -+ { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, -+ (0, import_react45.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react45.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), -+ (0, import_react45.createElement)("input", { -+ ref: input, -+ type: "text", -+ spellCheck: false, -+ autoComplete: "off", -+ "aria-label": t("Address"), -+ placeholder: t("Search or enter address"), -+ value: editing ? draft : shortAddress(page.url), -+ onFocus: (event) => { -+ setDraft(page.url); -+ const target = event.target; -+ requestAnimationFrame(() => target.select()); -+ }, -+ onBlur: () => setDraft(null), -+ onChange: (event) => setDraft(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ setDraft(null); -+ event.currentTarget.blur(); -+ } -+ } -+ }) -+ ), -+ page.loading ? (0, import_react45.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null -+ ), -+ (0, import_react45.createElement)( -+ "div", -+ { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, -+ page.error ? (0, import_react45.createElement)( -+ "div", -+ { className: "bt-browser-empty" }, -+ (0, import_react45.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), -+ (0, import_react45.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), -+ page.url ? (0, import_react45.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { -+ void actions.browserNavigate(tabId, page.url); -+ } }, t("Try again")) : null -+ ) : !page.url ? (0, import_react45.createElement)( -+ "div", -+ { className: "bt-browser-empty" }, -+ (0, import_react45.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), -+ (0, import_react45.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react45.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) -+ ) : null -+ ) -+ ); -+} -+function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) { -+ const bot = roster.byId[pane.id]; -+ const canBrowse = Boolean(bot && roster.browser === true && actions.browserShow); -+ const view = pane.view === "browser" && canBrowse ? "browser" : "details"; -+ const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); -+ const active = tabState.active; -+ const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); -+ const root = (0, import_react45.useRef)(null); -+ const fly = (0, import_react45.useRef)(null); -+ const [landed, setLanded] = (0, import_react45.useState)(false); -+ const [settling, setSettling] = (0, import_react45.useState)(false); -+ const [width, setWidth] = (0, import_react45.useState)(savedWidth); -+ const [viewport, setViewport] = (0, import_react45.useState)(() => window.innerWidth); -+ const [dragging, setDragging] = (0, import_react45.useState)(false); -+ const [resizing, setResizing] = (0, import_react45.useState)(false); -+ const instant = dragging || resizing; -+ const [reading, setReading] = (0, import_react45.useState)(false); -+ const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); -+ (0, import_react45.useLayoutEffect)(() => { -+ if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); -+ }, [view, width]); -+ (0, import_react45.useLayoutEffect)(() => { -+ chatRoom.hold(instant); -+ }, [instant]); -+ const [last, setLast] = (0, import_react45.useState)({ view, width: panelWidth }); -+ if (last.view !== view || last.width !== panelWidth) { -+ setLast({ view, width: panelWidth }); -+ if (last.view !== view) setSettling(last.width !== panelWidth && !instant); - } -- return null; --}; --var decisionOf = (file) => { -- if (file === null || typeof file !== "object") return null; -- const path = text(file.path); -- if (path === "") return null; -- return { -- key: `decision:${path}`, -- path, -- name: text(file.name) || path.slice(path.lastIndexOf("/") + 1), -- date: text(file.date), -- title: text(file.title), -- sha256: text(file.sha256), -- shortHash: text(file.sha256).slice(0, 12) -- }; --}; --var meetingOf = (meeting, status) => { -- if (meeting === null || typeof meeting !== "object") return null; -- const id = text(meeting.id); -- if (id === "") return null; -- return { -- key: `meeting:${id}`, -- id, -- date: text(meeting.date), -- question: text(meeting.question) || text(meeting.title), -- owner: text(meeting.owner) || null, -- status, -- path: text(meeting.path), -- blockingQuestion: text(meeting.blockingQuestion) || null, -- // #1831: the level and recipients the host answered, and the refusal to advance a severe card. -- // Nothing is inferred here: a record written before the slice carries an ordinary card and no -- // recipients, exactly as the host reads it. -- level: text(meeting.level) || "ordinary", -- recipients: Array.isArray(meeting.recipients) ? meeting.recipients.map((item) => text(item)).filter((name) => name !== "") : [], -- advance: meeting.advance !== null && typeof meeting.advance === "object" ? { allowed: meeting.advance.allowed === true, reason: text(meeting.advance.reason) || null } : { allowed: true, reason: null }, -- // #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 !== "") : [] -- }; --}; --var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; --function projectBoardRecord({ decisions, meetings } = {}) { -- const dir = text(decisions?.dir) || null; -- const files = listOf(decisions?.files); -- const decided = Array.isArray(meetings?.decided) ? meetings.decided.map((item) => meetingOf(item, "decided")).filter((item) => item !== null) : []; -- const pending = Array.isArray(meetings?.pending) ? meetings.pending.map((item) => meetingOf(item, item?.status ?? "deliberating")).filter((item) => item !== null) : []; -- return { -- decisions: { -- dir, -- files, -- count: files.length, -- empty: files.length === 0, -- emptyText: text(decisions?.reason) || (dir === null ? "This Project names no working folder." : `No decision file in ${dir}/decisions.`) -- }, -- meetings: { -- decided, -- pending, -- count: decided.length + pending.length, -- empty: decided.length === 0 && pending.length === 0, -- emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." -+ const shownView = (0, import_react45.useRef)(view); -+ (0, import_react45.useLayoutEffect)(() => { -+ const switched = settling && shownView.current !== view; -+ shownView.current = view; -+ if (switched) chatRoom.hold(true); -+ chatRoom.set(leaving ? null : panelWidth); -+ if (switched) chatRoom.hold(false); -+ }, [view, panelWidth, leaving]); -+ (0, import_react45.useEffect)(() => () => { -+ chatRoom.set(null); -+ chatRoom.hold(false); -+ }, []); -+ (0, import_react45.useEffect)(() => { -+ let timer; -+ const onResize = () => { -+ setResizing(true); -+ setViewport(window.innerWidth); -+ clearTimeout(timer); -+ timer = setTimeout(() => setResizing(false), 160); -+ }; -+ window.addEventListener("resize", onResize); -+ return () => { -+ clearTimeout(timer); -+ window.removeEventListener("resize", onResize); -+ }; -+ }, []); -+ (0, import_react45.useEffect)(() => { -+ if (!settling) return void 0; -+ const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); -+ return () => clearTimeout(timer); -+ }, [settling, view]); -+ (0, import_react45.useEffect)(() => { -+ const timer = setTimeout(() => setLanded(true), 360); -+ return () => clearTimeout(timer); -+ }, []); -+ (0, import_react45.useEffect)(() => { -+ if (canBrowse) actions.browserEnsure(bot.id); -+ }, [bot?.id, canBrowse]); -+ (0, import_react45.useEffect)(() => { -+ if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); -+ }, [view, active, tabState.ids.length]); -+ (0, import_react45.useEffect)(() => { -+ if (view !== "browser" || leaving) void actions.browserShow?.(null); -+ }, [view, leaving]); -+ (0, import_react45.useEffect)(() => () => { -+ void actions.browserShow?.(null); -+ }, []); -+ (0, import_react45.useEffect)(() => { -+ if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; -+ setReading(true); -+ const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -+ return () => clearTimeout(timer); -+ }, [page.readAt]); -+ const beginSwitch = (next) => { -+ if (reducedMotion2()) return; -+ const panel = root.current; -+ const outgoing = panel?.querySelector(".bt-panel-view"); -+ if (outgoing) { -+ const ghostEl = document.createElement("div"); -+ ghostEl.className = "bt-panel-ghost"; -+ ghostEl.style.width = `${outgoing.offsetWidth}px`; -+ ghostEl.append(outgoing.cloneNode(true)); -+ outgoing.parentElement.append(ghostEl); -+ void ghostEl.animate([ -+ { opacity: 1, transform: "scale(1)" }, -+ { opacity: 0, transform: "scale(0.985)" } -+ ], { duration: 160, easing: "ease-out", fill: "forwards" }).finished.catch(() => { -+ }).finally(() => ghostEl.remove()); - } -+ const source = next === "browser" ? panel?.querySelector(".bt-drawer-id .bt-mark") : panel?.querySelector(".bt-ptab-bot .bt-mark"); -+ if (source) fly.current = { node: source.cloneNode(true), from: source.getBoundingClientRect() }; - }; --} --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])); -- const list = []; -- const archived = []; -- const seen = /* @__PURE__ */ new Set(); -- for (const row of [...Array.isArray(done) ? done : [], ...Array.isArray(rows) ? rows : []]) { -- if (!isRow(row)) continue; -- const id = text(row.id); -- if (seen.has(id)) continue; -- seen.add(id); -- const column = text(row.column) || "not-started"; -- (column === DONE_COLUMN ? archived : list).push(row); -+ const switchView = (next) => { -+ if (next === view || leaving) return; -+ beginSwitch(next); -+ actions.setPanelView(next); -+ }; -+ const panelActions = { ...actions }; -+ for (const name of ["openBrowser", "browserNewTab", "browserPick"]) { -+ if (actions[name]) panelActions[name] = (...args) => { -+ if (view !== "browser") beginSwitch("browser"); -+ return actions[name](...args); -+ }; - } -- const projectRow = (row) => { -- const task = byId.get(text(row.id)); -- return { -- id: text(row.id), -- key: `task:${text(row.id)}`, -- column: text(row.column) || "not-started", -- assignee: text(row.assignee) || null, -- runId: text(row.runId) || null, -- nodeId: text(row.nodeId) || null, -- title: text(task?.title) || text(task?.note) || null, -- priority: text(task?.priority) || null, -- audit: text(row.audit) || null, -- // #1831: who is doing the card, and who is speaking for whom. Both are host truth; -- // a card carrying neither renders exactly as before. -- holder: text(row.holder) || null, -- speaksFor: text(row.speaksFor) || null, -- board: boardOfRow(row, task) -+ (0, import_react45.useLayoutEffect)(() => { -+ const move = fly.current; -+ if (!move) return; -+ fly.current = null; -+ const target = view === "browser" ? root.current?.querySelector(".bt-ptab-bot .bt-mark") : root.current?.querySelector(".bt-drawer-id .bt-mark"); -+ if (!target || move.from.width === 0) return; -+ const growing = root.current.getAnimations({ subtree: true }).filter((animation) => animation.animationName === "bt-strip-in"); -+ const at = growing.map((animation) => animation.currentTime); -+ for (const animation of growing) animation.currentTime = animation.effect.getComputedTiming().endTime; -+ const to = target.getBoundingClientRect(); -+ growing.forEach((animation, index) => { -+ animation.currentTime = at[index]; -+ }); -+ const clone = move.node; -+ Object.assign(clone.style, { -+ position: "fixed", -+ left: `${move.from.left}px`, -+ top: `${move.from.top}px`, -+ width: `${move.from.width}px`, -+ height: `${move.from.height}px`, -+ margin: "0", -+ transformOrigin: "0 0", -+ zIndex: "80", -+ pointerEvents: "none", -+ visibility: "" -+ }); -+ document.body.append(clone); -+ target.style.visibility = "hidden"; -+ const scale = to.width / move.from.width; -+ const anim = clone.animate([ -+ { transform: "translate(0px, 0px) scale(1)" }, -+ { transform: `translate(${to.left - move.from.left}px, ${to.top - move.from.top}px) scale(${scale})` } -+ ], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, fill: "forwards" }); -+ void anim.finished.catch(() => { -+ }).finally(() => { -+ clone.remove(); -+ target.style.visibility = ""; -+ }); -+ }, [view, pane.id]); -+ const startResize = (event) => { -+ if (event.button !== 0 || leaving) return; -+ event.preventDefault(); -+ const panel = root.current; -+ const grip = event.currentTarget; -+ grip.setPointerCapture?.(event.pointerId); -+ const range = widthRange(view, window.innerWidth); -+ const from = { x: event.clientX, width: panelWidth }; -+ let next = panelWidth; -+ setDragging(true); -+ setSettling(false); -+ chatRoom.hold(true); -+ const columns = chatRoom.columns(); -+ for (const column of columns) column.style.setProperty("transition", "none", "important"); -+ const onMove = (move) => { -+ next = fitWidth(from.width + from.x - move.clientX, range); -+ panel.style.width = `${next}px`; -+ if (columns.length === 0) chatRoom.set(next); -+ for (const column of columns) column.style.setProperty("margin-right", `${next}px`, "important"); -+ }; -+ const ends = ["pointerup", "pointercancel", "lostpointercapture"]; -+ const onEnd = (end) => { -+ if (end.type === "pointerup") onMove(end); -+ grip.removeEventListener("pointermove", onMove); -+ for (const name of ends) grip.removeEventListener(name, onEnd); -+ chatRoom.set(next); -+ for (const column of columns) { -+ column.style.removeProperty("margin-right"); -+ column.style.removeProperty("transition"); -+ } -+ panel.style.setProperty("--bt-panel-w", `${next}px`); -+ panel.style.removeProperty("width"); -+ setDragging(false); -+ if (next === from.width) return; -+ setWidth(next); -+ try { -+ localStorage.setItem(WIDTH_KEY, String(next)); -+ } catch { -+ } - }; -+ grip.addEventListener("pointermove", onMove); -+ for (const name of ends) grip.addEventListener(name, onEnd); - }; -- const groups = PROJECT_SOURCES.map((source) => ({ -- source, -- label: SOURCE_LABELS[source], -- rows: list.filter((row) => row.source === source).map(projectRow) -- })).filter((group) => group.rows.length > 0); -- const countOf = (value) => value === void 0 || value === null ? null : value; -- const queueItems = (items, annotate) => (Array.isArray(items) ? items : []).filter((item) => item !== null && typeof item === "object" && text(item.id) !== "").map((item) => { -- const id = text(item.id); -- const source = text([...list, ...archived].find((row) => text(row.id) === id)?.source); -- const task = byId.get(id); -- return { id, key: `task:${id}`, title: text(task?.title) || text(task?.note) || null, priority: text(task?.priority) || null, board: boardOfRow({ source }, task), ...annotate(item) }; -- }); -- const waiting = queueItems(queues?.waiting, (item) => ({ role: text(item.role) || null, waitedMs: Number.isFinite(item.waitedMs) ? item.waitedMs : null })); -- const inflight = queueItems(queues?.inflight, (item) => ({ holder: text(item.holder) || null, state: item.stalled === true ? "stalled" : "running" })); -- const blocked = queueItems(queues?.blocked, (item) => ({ reason: text(item.reason) || null })); -- const queueGroups = [ -- { group: "waiting", label: "Waiting", items: waiting }, -- { group: "inflight", label: "In flight", items: inflight }, -- { group: "blocked", label: "Blocked", items: blocked } -- ].filter((entry) => entry.items.length > 0); -- const rawCounts = queues?.counts ?? null; -- return { -- projectId: text(project?.id) || null, -- title: text(project?.name) || "Project", -- canManage: canManage === true, -- groups, -- // #1855: one group, closed until the reader opens it. Rows keep the row shape above, so a -- // finished card is still a card — only its place on the board changed. -- done: { group: "done", label: "Done cards", collapsed: true, rows: archived.map(projectRow) }, -- queues: queueGroups, -- counts: rawCounts === null ? null : { waiting: countOf(rawCounts.waiting), inflight: countOf(rawCounts.inflight), blocked: countOf(rawCounts.blocked), done: countOf(rawCounts.done) }, -- count: list.length, -- empty: list.length === 0 && queueGroups.length === 0 && archived.length === 0, -- emptyText: "No cards yet.", -- record: projectBoardRecord({ decisions, meetings }), -- metrics: metricsView(metrics, { tasks }) -- }; --} --function projectRowNode(row, onOpen) { -- const open = row.board === null ? void 0 : () => onOpen?.(row.board); -- return (0, import_react47.createElement)( -- "div", -+ const hasStrip = canBrowse && tabState.ids.length > 0; -+ return (0, import_react45.createElement)( -+ "aside", - { -- key: row.key, -- className: "bt-project-row", -- "data-task-id": row.id, -- "data-column": row.column, -- "data-priority": row.priority ?? void 0, -- role: open === void 0 ? void 0 : "button", -- tabIndex: open === void 0 ? void 0 : 0, -- onClick: open, -- onKeyDown: open === void 0 ? void 0 : (event) => { -- if (event.key === "Enter" || event.key === " ") open(); -+ ref: root, -+ className: "bt-panel", -+ "data-view": view, -+ "aria-label": view === "browser" && bot ? t("{name}'s browser", { name: bot.name }) : t("Conversation details"), -+ "data-leaving": leaving || void 0, -+ "aria-hidden": leaving || void 0, -+ "data-settling": settling || void 0, -+ "data-instant": instant || void 0, -+ "data-dragging": dragging || void 0, -+ style: { "--bt-panel-w": `${panelWidth}px` }, -+ onAnimationEnd: (event) => { -+ if (event.target === event.currentTarget) setLanded(true); - } - }, -- (0, import_react47.createElement)("span", { className: "bt-project-column" }, t(row.column)), -- (0, import_react47.createElement)( -- "span", -- { className: "bt-project-title-wrap" }, -- (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), -- // 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_react47.createElement)("span", { className: "bt-project-audit" }, row.audit) -- ), -- // "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_react47.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), -- row.speaksFor === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), -- row.priority === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-priority" }, row.priority), -- row.runId === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) -+ leaving ? null : (0, import_react45.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), -+ // Over the whole window while dragging: the cursor stays a resize cursor and the -+ // pages underneath (the chat, the guest) see no pointer. -+ dragging ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, -+ hasStrip ? (0, import_react45.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, -+ view === "browser" && bot ? (0, import_react45.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react45.createElement)( -+ "div", -+ { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, -+ (0, import_react45.createElement)(DetailsDrawer, { -+ sessionId: pane.id, -+ startRenaming: pane.renaming, -+ startTab: pane.tab, -+ hasStrip, -+ roster, -+ actions: panelActions, -+ useSessions, -+ useSessionStatus, -+ useActivity, -+ useWork, -+ useBrowserTabs, -+ useBrowserPages -+ }) -+ ) - ); - } --function ProjectBoardRows({ view, onOpen }) { -- if (view.empty) return (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); -- return view.groups.map((group) => (0, import_react47.createElement)( -- "section", -- { key: group.source, className: "bt-project-group", "data-source": group.source }, -- (0, import_react47.createElement)("h4", null, t(group.label)), -- group.rows.map((row) => projectRowNode(row, onOpen)) -- )); -+var BROWSER_CSS = ` -+.bt-panel{--bt-panel-view-fade:${SPRING_SHAPE.duration}ms} -+.bt-ptabs{position:relative;display:flex;align-items:center;height:44px;flex:none;border-bottom:.5px solid var(--bt-line);padding:0 6px;gap:0;animation:bt-strip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} -+@keyframes bt-strip-in{from{height:0;opacity:0}} -+.bt-ptabs-in{position:relative;display:flex;align-items:center;gap:2px;flex:1;min-width:0;height:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;padding:4px 2px;scroll-padding-left:48px;mask-image:linear-gradient(to right,#000,#000 calc(100% - 10px),transparent)} -+.bt-ptabs-in>.bt-ptab-bot{position:sticky;left:0;z-index:2;background:var(--bt-main)} -+.bt-ptabs-in>.bt-ptab-bot[aria-selected=true]{background:transparent} -+.bt-ptabs-in::-webkit-scrollbar{display:none} -+.bt-ptab{position:relative;z-index:1;display:inline-flex;align-items:center;gap:6px;height:32px;max-width:180px;padding:0 8px;flex:none;border:0;border-radius:9px;background:none;color:var(--bt-ink-2);font:inherit;font-size:12.5px;cursor:pointer;white-space:nowrap;transition:color .12s ease} -+.bt-ptab:hover{color:var(--bt-ink)} -+.bt-ptab[aria-selected=true]{color:var(--bt-ink)} -+.bt-ptab-bot{padding:0 7px} -+.bt-ptab-bot .bt-mark{flex:none} -+.bt-ptab-name{max-width:96px;overflow:hidden;text-overflow:ellipsis;font-weight:500;animation:bt-ptab-label .18s ease} -+@keyframes bt-ptab-label{from{opacity:0;filter:blur(4px);max-width:0}} -+.bt-ptab-fav{width:14px;height:14px;flex:none;border-radius:3px;object-fit:cover;color:var(--bt-ink-3)} -+.bt-ptab-globe{display:inline-flex} -+.bt-ptab-globe svg{width:14px;height:14px} -+.bt-ptab-title{max-width:140px;overflow:hidden;text-overflow:ellipsis} -+.bt-ptab-x{flex:none;width:16px;height:16px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;color:var(--bt-ink-3);opacity:0;transition:opacity .12s ease,background-color .12s ease} -+.bt-ptab:hover .bt-ptab-x,.bt-ptab[aria-selected=true] .bt-ptab-x{opacity:1} -+.bt-ptab-x:hover{background:var(--bt-active);color:var(--bt-ink)} -+.bt-ptab-new{position:relative;z-index:1;flex:none;width:28px;height:32px;border:0;border-radius:9px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer} -+.bt-ptab-new:hover{color:var(--bt-ink)} -+.bt-ptab-new svg{width:15px;height:15px} -+.bt-ptab-ind{position:absolute;top:4px;bottom:4px;border-radius:9px;background:var(--bt-hover);opacity:0;pointer-events:none;z-index:0;transition:opacity .15s ease} -+.bt-ptab-close{flex:none;margin-left:2px} -+.bt-panel-ghost{position:absolute;top:44px;right:0;bottom:0;overflow:hidden;pointer-events:none;z-index:2} -+.bt-panel-ghost>.bt-panel-view{position:absolute;inset:0} -+.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column;animation:bt-panel-view-in .22s cubic-bezier(.16,1,.3,1)} -+@keyframes bt-panel-view-in{from{opacity:0}} -+/* While the panel changes width its views keep their final width on the right edge, so -+ only the panel edge moves and nothing inside reflows frame by frame. */ -+.bt-panel[data-settling]{overflow:hidden} -+.bt-panel[data-settling]>.bt-ptabs,.bt-panel[data-settling]>.bt-panel-view{flex-shrink:0;align-self:flex-end;box-sizing:border-box;width:calc(var(--bt-panel-w,320px) - .5px)} -+.bt-panel-browser{position:relative} -+.bt-browser-bar{position:relative;display:flex;align-items:center;gap:2px;height:52px;padding:0 10px 0 12px;flex:none;border-bottom:.5px solid var(--bt-line)} -+.bt-browser-bar .bt-icon-btn:disabled{opacity:.35;cursor:default;background:none} -+.bt-browser-address{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:32px;margin:0 4px;padding:0 12px;border-radius:999px;background:var(--bt-hover);border:.5px solid transparent;transition:background-color .12s ease,border-color .12s ease,box-shadow .12s ease} -+.bt-browser-address:hover{background:var(--bt-active)} -+.bt-browser-address[data-editing]{background:var(--bt-main);border-color:var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04)} -+.bt-browser-scheme{display:inline-flex;color:var(--bt-ink-3);flex:none} -+.bt-browser-scheme svg{width:14px;height:14px} -+.bt-browser-address input{flex:1;min-width:0;border:0;background:none;outline:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;padding:0;text-overflow:ellipsis} -+.bt-browser-address input::placeholder{color:var(--bt-ink-3)} -+.bt-browser-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;pointer-events:none} -+.bt-browser-progress::after{content:"";position:absolute;inset:0;width:40%;background:var(--bt-accent,#4D6BFE);border-radius:2px;animation:bt-browser-load 1.1s cubic-bezier(.4,0,.2,1) infinite} -+@keyframes bt-browser-load{from{transform:translateX(-100%)}to{transform:translateX(250%)}} -+.bt-browser-frame{position:relative;flex:1;min-height:0;background:var(--bt-main)} -+.bt-browser-frame[data-guest]{background:#fff} -+.bt-browser-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:32px;text-align:center;animation:bt-foot-in .3s ease} -+.bt-browser-empty-title{font-size:15px;line-height:22px;font-weight:500;margin-top:6px} -+.bt-browser-empty-text{max-width:300px;font-size:13px;line-height:19px;color:var(--bt-ink-2);overflow-wrap:anywhere} -+.bt-browser-retry{margin-top:6px;height:32px;padding:0 14px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer} -+.bt-browser-retry:hover{background:var(--bt-hover)} -+.bt-bcards{display:flex;flex-direction:column;gap:12px} -+.bt-bcard{position:relative;border:.5px solid var(--bt-line-2);border-radius:12px;overflow:hidden;background:var(--bt-main);animation:bt-bcard-in .24s cubic-bezier(.16,1,.3,1) both;transition:border-color .12s ease,box-shadow .12s ease} -+@keyframes bt-bcard-in{from{opacity:0;filter:blur(4px);transform:translateY(4px)}} -+.bt-bcard[data-active]{border-color:var(--bt-accent,#4D6BFE);box-shadow:0 0 0 1px var(--bt-accent,#4D6BFE)} -+.bt-bcard-shot{position:relative;display:block;width:100%;aspect-ratio:16/10;border:0;background:var(--bt-hover);padding:0;cursor:pointer} -+.bt-bcard-blank{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--bt-ink-3);font-size:12px} -+.bt-bcard-blank svg{width:20px;height:20px} -+.bt-bcard-preview{position:absolute;inset:0;display:flex;flex-direction:column;gap:6px;padding:14px 16px;text-align:left;background:linear-gradient(to bottom,var(--bt-main),var(--bt-hover));overflow:hidden} -+.bt-bcard-ptitle{font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} -+.bt-bcard-excerpt{font-size:12px;line-height:17px;color:var(--bt-ink-2);display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} -+.bt-bcard-row{display:flex;align-items:center;gap:7px;padding:8px 10px;min-width:0} -+.bt-bcard-fav{width:14px;height:14px;flex:none;border-radius:3px;color:var(--bt-ink-3)} -+.bt-bcard-row svg.bt-bcard-fav{width:14px;height:14px} -+.bt-bcard-title{flex:1;min-width:0;font-size:12.5px;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink)} -+.bt-bcard-host{flex:none;font-size:11px;line-height:15px;color:var(--bt-ink-3);max-width:38%;overflow:hidden;text-overflow:ellipsis} -+.bt-bcard-x{position:absolute;top:6px;right:6px;width:22px;height:22px;border:0;border-radius:7px;background:color-mix(in srgb,var(--bt-main) 88%,transparent);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;font-size:14px;cursor:pointer;opacity:0;transition:opacity .12s ease;backdrop-filter:blur(4px)} -+.bt-bcard:hover .bt-bcard-x{opacity:1} -+.bt-bcard-new{display:flex;align-items:center;justify-content:center;gap:6px;height:40px;border:.5px dashed var(--bt-line-2);border-radius:12px;background:none;color:var(--bt-ink-3);font:inherit;font-size:12.5px;cursor:pointer} -+.bt-bcard-new:hover{color:var(--bt-ink);border-color:var(--bt-ink-3)} -+.bt-bcard-new svg{width:14px;height:14px} -+.bt-browser-empty-mark{display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center} -+@media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} -+`; -+ -+// src/client/remote-bot-settings.js -+var import_react46 = require("react"); -+var REMOTE_BOT_SETTINGS_CSS = ` -+.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} -+.bt-remote-sheet-layer[data-leaving]{pointer-events:none} -+.bt-remote-sheet-layer .bt-scrim{position:absolute;inset:0;z-index:0;pointer-events:auto} -+.bt-remote-sheet-layer[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} -+.bt-remote-sheet:focus{outline:none} -+.bt-remote-sheet{position:relative;z-index:1;pointer-events:auto;width:min(320px,calc(100vw - 32px));max-height:min(70vh,420px);margin:0 auto 0;border-radius:14px;background:var(--bt-card,#fff);border:.5px solid var(--bt-line-2);box-shadow:0 12px 40px rgba(20,20,20,.16);display:flex;flex-direction:column;overflow:hidden;animation:bt-remote-sheet-in .2s ease-out} -+.bt-remote-sheet-layer[data-leaving] .bt-remote-sheet{animation:bt-remote-sheet-out .16s ease-in forwards} -+.bt-remote-sheet-head{display:flex;align-items:center;gap:10px;padding:12px 12px 8px} -+.bt-remote-sheet-who{display:flex;flex-direction:column;min-width:0;gap:1px;flex:1 1 auto} -+.bt-remote-sheet-name{font-size:16px;line-height:22px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-remote-sheet-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-remote-sheet-body{padding:0 12px 14px;overflow:auto} -+@keyframes bt-remote-sheet-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} -+@keyframes bt-remote-sheet-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(8px)}} -+@media (min-width:520px){ -+.bt-remote-sheet-layer{align-items:center;padding:24px} -+.bt-remote-sheet{margin:0} - } --function ProjectDoneGroup({ view, onOpen }) { -- if (view.done.rows.length === 0) return null; -+`; -+function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { -+ const close = () => actions.closeSettings(); -+ useEscape(close); -+ const dialog = (0, import_react46.useRef)(null); -+ (0, import_react46.useEffect)(() => { -+ const before = document.activeElement; -+ dialog.current?.focus?.({ preventScroll: true }); -+ return () => { -+ if (before && typeof before.focus === "function" && before.isConnected) before.focus({ preventScroll: true }); -+ }; -+ }, []); -+ const onKeyDown = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ close(); -+ } -+ }; -+ if (!bot?.remote) return null; -+ const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); -+ return (0, import_react46.createElement)( -+ "div", -+ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, -+ (0, import_react46.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react46.createElement)( -+ "div", -+ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, -+ (0, import_react46.createElement)( -+ "div", -+ { className: "bt-remote-sheet-head" }, -+ (0, import_react46.createElement)(BotMark, { bot, size: 40, live: true }), -+ (0, import_react46.createElement)( -+ "div", -+ { className: "bt-remote-sheet-who" }, -+ (0, import_react46.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), -+ (0, import_react46.createElement)("span", { className: "bt-remote-sheet-sub" }, who) -+ ), -+ (0, import_react46.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react46.createElement)(CloseIcon)) -+ ), -+ (0, import_react46.createElement)( -+ "div", -+ { className: "bt-remote-sheet-body" }, -+ bot.remote.handle ? (0, import_react46.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, -+ (0, import_react46.createElement)(FlyBotDetails, { bot, roster, actions }) -+ ) -+ ) -+ ); -+} -+ -+// src/client/overlays.js -+var import_react47 = require("react"); -+var import_react_dom11 = require("react-dom"); -+var DIALOG_EXIT_MS = 160; -+var MENU_EXIT_MS3 = 120; -+function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -+ const roster = useRoster((value) => value); -+ const ui2 = useUi((value) => value); -+ useRegistry((value) => value); -+ const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); -+ const previous = (0, import_react47.useRef)(current); -+ (0, import_react47.useEffect)(() => { -+ if (previous.current === current) return; -+ previous.current = current; -+ if (current) actions.followPanel?.(current); -+ }, [current]); -+ const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; -+ const [kept, setKept] = (0, import_react47.useState)(open); -+ (0, import_react47.useEffect)(() => { -+ if (open) { -+ setKept(open); -+ return void 0; -+ } -+ const timer = setTimeout(() => setKept(null), PANEL_LINGER_MS); -+ return () => clearTimeout(timer); -+ }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); -+ const pane = open ?? kept; -+ const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); -+ const remoteBotId = ui2.remoteBotSettings && roster.byId[ui2.remoteBotSettings]?.remote ? ui2.remoteBotSettings : null; -+ const [remoteBotSettings, remoteLeaving] = useLinger(remoteBotId, DIALOG_EXIT_MS); -+ const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS); -+ const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); -+ const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); -+ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); - return (0, import_react47.createElement)( -- "details", -- { className: "bt-project-group bt-project-done", "data-group": "done" }, -- (0, import_react47.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), -- view.done.rows.map((row) => projectRowNode(row, onOpen)) -+ import_react47.Fragment, -+ null, -+ pane ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, -+ ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, -+ ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, -+ ui2.exchange ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, -+ ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, -+ menu ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, -+ settings ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, -+ remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, -+ memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, -+ inbox ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, -+ board ? (0, import_react_dom11.createPortal)((0, import_react47.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null - ); - } --function waitedLabel(ms) { -- if (!Number.isFinite(ms) || ms < 0) return null; -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${minutes}m`; -- const hours = Math.floor(minutes / 60); -- if (hours < 24) return `${hours}h`; -- return `${Math.floor(hours / 24)}d`; --} --var METRIC_CARDS = [ -- { key: "prsOpenedToday", label: "PRs opened today", caption: "Natural day in your time zone, by created_at." }, -- { key: "prsMergedToday", label: "PRs merged today", caption: "Natural day in your time zone, by merged_at." }, -- { key: "issuesOpenedToday", label: "Issues opened today", caption: "Natural day in your time zone, by created_at." }, -- { key: "issuesClosedToday", label: "Issues closed today", caption: "Natural day in your time zone, by closed_at." }, -- { key: "prMergeMedianMs", label: "PR merge time", caption: "Median of the last 7 days, opened to merged." }, -- { key: "issueCloseMedianMs", label: "Issue close time", caption: "Median of the last 7 days, opened to closed." }, -- { key: "taskDoneMedianMs", label: "Task completion", caption: "Median of the last 7 days, created to done." } --]; --var METRIC_DASH = "—"; --var DURATION_KEYS = /* @__PURE__ */ new Set(["prMergeMedianMs", "issueCloseMedianMs", "taskDoneMedianMs"]); --var RUNNING_OVERDUE_MS = 2 * 60 * 60 * 1e3; --var metricText = (key, value) => { -- if (!Number.isFinite(value)) return METRIC_DASH; -- if (DURATION_KEYS.has(key)) return waitedLabel(value) ?? METRIC_DASH; -- return String(value); -+ -+// src/client/read-page.js -+var READ_LIMITS = { -+ chars: 4e5, -+ nodes: 15e4, -+ depth: 400, -+ ms: 1500, -+ items: 3e3, -+ tableRows: 5e3, -+ tableCols: 30, -+ frameDepth: 3 - }; --function metricsView(metrics, { tasks } = {}) { -- const source = metrics !== null && typeof metrics === "object" ? metrics : null; -- const values = source?.metrics !== null && typeof source?.metrics === "object" ? source.metrics : {}; -- const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); -- const running = (Array.isArray(source?.running) ? source.running : []).filter((row) => row !== null && typeof row === "object" && text(row.id) !== "").map((row) => { -- const id = text(row.id); -- const task = byId.get(id); -- return { -- id, -- key: "task:" + id, -- title: text(row.title) || text(task?.title) || text(task?.note) || id, -- status: text(row.status), -- who: text(row.who) || null, -- sinceMs: Number.isFinite(row.sinceMs) ? row.sinceMs : null, -- source: text(row.source) || null, -- 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) -- }; -- }); -- return { -- available: source?.ok === true, -- reason: text(source?.reason) || null, -- cards: METRIC_CARDS.map((card) => ({ ...card, value: metricText(card.key, values[card.key]) })), -- running, -- empty: source === null, -- emptyText: "No metrics read yet." -+function readPage(limits) { -+ const started = performance.now(); -+ const SKIP = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link", "svg", "object", "embed", "audio", "video", "source", "track", "map", "datalist", "option", "optgroup"]); -+ const ITEM = 'a[href], button, input:not([type=hidden]), textarea, select, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=checkbox], [role=switch], [role=radio], [contenteditable=""], [contenteditable=true]'; -+ const ROLES = { a: "link", button: "button", select: "select", textarea: "textbox", summary: "button" }; -+ const INPUT_ROLES = { checkbox: "checkbox", radio: "radio", submit: "button", button: "button", reset: "button", image: "button", search: "searchbox", range: "slider", file: "button", color: "button" }; -+ const NO_VALUE_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio", "submit", "button", "reset", "image", "file", "color"]); -+ const SECRET_HINT = /(^|[-_\s])(cvv|cvc|csc|cc-?(num|number|csc|exp)|card-?(num|number)|otp|one-?time-?code|pin)([-_\s]|$)/i; -+ const BLOCK_DISPLAY = /^(block|flex|grid|list-item|table|flow-root|table-caption|table-row-group|table-header-group|table-footer-group|table-row|-webkit-box)$/; -+ const INLINE_BLOCK = /^(inline-block|inline-flex|inline-grid|inline-table|table-cell|-webkit-inline-box)$/; -+ const SEMANTIC = /^(ul|ol|table|pre|blockquote|h[1-6]|code|details)$/; -+ const items = []; -+ const stash = []; -+ const stats = { nodes: 0, frames: 0 }; -+ let chars = 0; -+ let stop = ""; -+ const collapse = (value) => String(value || "").replace(/\s+/g, " ").trim(); -+ const isSecret = (el) => { -+ if (el.localName !== "input" && el.localName !== "textarea") return false; -+ const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); -+ return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); -+ }; -+ const keep = (text3, end = "") => { -+ if (text3 === "") return ""; -+ stash.push(text3); -+ return ` -+ -+${stash.length - 1}${end} -+ -+`; -+ }; -+ const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); -+ const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); -+ const block = (text3) => expand(flow(text3)); -+ const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); -+ const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); -+ const childrenOf = (el) => { -+ if (el.shadowRoot) return el.shadowRoot.childNodes; -+ if (el.localName === "slot" && typeof el.assignedNodes === "function") { -+ const assigned = el.assignedNodes(); -+ if (assigned.length > 0) return assigned; -+ } -+ return el.childNodes; -+ }; -+ const over = () => { -+ if (stop) return true; -+ if (chars > limits.chars) stop = `the text passed ${limits.chars} characters`; -+ else if (stats.nodes > limits.nodes) stop = `the page has more than ${limits.nodes} nodes`; -+ else if (stats.nodes % 512 === 0 && performance.now() - started > limits.ms) stop = `reading took longer than ${limits.ms} ms`; -+ return stop !== ""; -+ }; -+ const roleOf = (el) => el.getAttribute("role") || ROLES[el.localName] || (el.localName === "input" ? INPUT_ROLES[el.type] || "textbox" : el.isContentEditable ? "textbox" : el.localName); -+ const sized = (el) => { -+ const box = el.getBoundingClientRect(); -+ return box.width > 0 && box.height > 0; -+ }; -+ const itemFor = (el, style) => { -+ if (items.length >= limits.items || style.visibility !== "visible" || !el.matches(ITEM) || !sized(el)) return null; -+ const item = { role: roleOf(el) }; -+ const secret = isSecret(el); -+ if (el.href) item.href = String(el.href); -+ if ((el.localName === "input" || el.localName === "textarea") && !secret && !NO_VALUE_TYPES.has(el.type) && el.value) item.value = String(el.value).slice(0, 120); -+ if (el.localName === "select" && el.selectedOptions?.length) item.value = collapse([...el.selectedOptions].map((option) => option.label || option.text).join(", ")).slice(0, 120); -+ if (el.checked) item.checked = true; -+ if (el.disabled) item.disabled = true; -+ items.push(item); -+ return { item, number: items.length, secret }; -+ }; -+ const nameOf = (el, text3, secret) => collapse( -+ el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" -+ ).slice(0, 80); -+ const fenced = (code, lang) => { -+ const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); -+ const fence = "`".repeat(longest + 1); -+ return `${fence}${lang} -+${code.replace(/\n+$/, "")} -+${fence}`; -+ }; -+ const langOf = (el) => { -+ const names = `${el.className || ""} ${el.querySelector?.("code")?.className || ""}`; -+ return /(?:^|\s)(?:lang|language)-([\w+#.-]+)/.exec(names)?.[1] || ""; -+ }; -+ const list = (el, ctx) => { -+ const ordered = el.localName === "ol"; -+ let number = ordered ? Number(el.getAttribute("start") || 1) || 1 : 1; -+ const lines = []; -+ for (const child of childrenOf(el)) { -+ if (over()) break; -+ if (child.nodeType !== 1 || child.localName !== "li") { -+ const loose = block(render(child, ctx)); -+ if (loose) lines.push(loose); -+ continue; -+ } -+ const style = styleOf(child); -+ if (style.display === "none") continue; -+ let body2 = block(children(child, { ...ctx, depth: ctx.depth + 1, shown: style.visibility === "visible" })); -+ if (!body2.includes("```")) body2 = body2.replace(/\n{2,}/g, "\n"); -+ const marker = ordered ? `${number++}. ` : "- "; -+ if (body2 === "") continue; -+ const pad2 = " ".repeat(marker.length); -+ lines.push(body2.split("\n").map((line2, index) => (index === 0 ? marker : line2 ? pad2 : "") + line2).join("\n")); -+ } -+ return keep(lines.join("\n")); -+ }; -+ const cellText = (cell, ctx) => block(children(cell, ctx)).replace(/\n+/g, "
").replace(/\|/g, "\\|"); -+ const layoutTable = (table, rows) => { -+ const role = table.getAttribute("role"); -+ if (role === "presentation" || role === "none") return true; -+ if (table.querySelector("table")) return true; -+ return !rows.some((row) => row.cells.length > 1); -+ }; -+ const tableMarkdown = (table, ctx) => { -+ const rows = [...table.rows].filter((row) => styleOf(row).display !== "none"); -+ const caption = table.caption ? oneLine(children(table.caption, ctx)) : ""; -+ if (layoutTable(table, rows)) { -+ return rows.map((row) => [...row.cells].map((cell) => ` -+ -+${children(cell, ctx)} -+ -+`).join("")).join(""); -+ } -+ const grid = []; -+ const carry = []; -+ let width = 0; -+ for (const row of rows.slice(0, limits.tableRows + 1)) { -+ if (over()) break; -+ const line2 = []; -+ let column = 0; -+ const place = () => { -+ while (carry[column]?.left > 0) { -+ line2[column] = carry[column].text; -+ carry[column].left -= 1; -+ column += 1; -+ } -+ }; -+ for (const cell of row.cells) { -+ place(); -+ if (styleOf(cell).display === "none") continue; -+ const text3 = cellText(cell, ctx); -+ const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); -+ const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); -+ for (let offset = 0; offset < span; offset += 1) { -+ line2[column] = offset === 0 ? text3 : ""; -+ if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; -+ column += 1; -+ } -+ } -+ place(); -+ width = Math.max(width, line2.length); -+ grid.push(line2); -+ } -+ width = Math.min(width, limits.tableCols); -+ if (grid.length === 0 || width === 0) return caption ? ` -+ -+${caption} -+ -+` : ""; -+ const format = (cells) => `| ${Array.from({ length: width }, (_, index) => cells[index] ?? "").join(" | ")} |`; -+ const out = [format(grid[0]), `|${" --- |".repeat(width)}`, ...grid.slice(1).map(format)]; -+ if (rows.length > limits.tableRows + 1) out.push(`… (table cut: ${rows.length - limits.tableRows - 1} more rows)`); -+ return `${caption ? ` -+ -+Table: ${caption}` : ""}${keep(out.join("\n"))}`; -+ }; -+ const frame = (el, ctx) => { -+ let doc = null; -+ try { -+ doc = el.contentDocument; -+ } catch { -+ doc = null; -+ } -+ const address = (() => { -+ try { -+ return doc?.location?.href || el.src || el.getAttribute("src") || "about:blank"; -+ } catch { -+ return el.src || "about:blank"; -+ } -+ })(); -+ if (!doc) return ` -+ -+[iframe (cross-origin, not readable): ${address}] -+ -+`; -+ if (ctx.frames >= limits.frameDepth) return ` -+ -+[iframe (nested too deep, not read): ${address}] -+ -+`; -+ stats.frames += 1; -+ const title = collapse(el.title || doc.title); -+ const body2 = doc.body ? block(children(doc.body, { ...ctx, frames: ctx.frames + 1, depth: ctx.depth + 1, shown: true, pre: false })) : ""; -+ return keep(`[iframe: ${title ? `${title} (${address})` : address}] -+${body2 || "(no text)"} -+[iframe end]`); - }; -+ function children(el, ctx) { -+ let out = ""; -+ for (const child of childrenOf(el)) { -+ if (over()) break; -+ out += render(child, ctx); -+ } -+ return out; -+ } -+ function render(node, ctx) { -+ stats.nodes += 1; -+ if (node.nodeType === 3) { -+ if (!ctx.shown) return ""; -+ const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); -+ const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); -+ chars += text3.length; -+ return text3; -+ } -+ if (node.nodeType !== 1) return ""; -+ const el = node; -+ const tag = el.localName; -+ if (SKIP.has(tag) || ctx.depth > limits.depth || over()) return ""; -+ const style = styleOf(el); -+ const display = style.display; -+ if (display === "none" || style.contentVisibility === "hidden") return ""; -+ const shown = style.visibility === "visible"; -+ const keepsSpace = String(style.whiteSpace || "").startsWith("pre") || style.whiteSpace === "break-spaces"; -+ const next = { ...ctx, depth: ctx.depth + 1, shown }; -+ if (display === "contents" && !SEMANTIC.test(tag) && !el.matches(ITEM)) return children(el, next); -+ if (style.overflowX !== "visible" || style.overflowY !== "visible") { -+ const box = el.getBoundingClientRect(); -+ if (box.width === 0 || box.height === 0) return ""; -+ } -+ if (tag === "br") return "\n"; -+ if (tag === "hr") return "\n\n---\n\n"; -+ if (tag === "img") { -+ const alt = collapse(el.getAttribute("alt")); -+ return shown && alt ? `[image: ${alt}]` : ""; -+ } -+ if (tag === "canvas") { -+ const box = el.getBoundingClientRect(); -+ return box.width >= 200 && box.height >= 100 ? "\n\n[canvas: drawn content, not readable]\n\n" : ""; -+ } -+ if (tag === "iframe" || tag === "frame") { -+ const box = el.getBoundingClientRect(); -+ return tag === "iframe" && (box.width < 2 || box.height < 2) ? "" : frame(el, next); -+ } -+ const found = itemFor(el, style); -+ const mark = found ? `[#${found.number}]` : ""; -+ if (tag === "input" || tag === "textarea" || tag === "select") { -+ if (found) found.item.name = nameOf(el, tag === "select" ? "" : el.labels?.[0] ? collapse(el.labels[0].textContent) : "", found.secret); -+ return mark ? ` ${mark} ` : ""; -+ } -+ let body2; -+ if (tag === "pre") { -+ const code = shown ? String(el.innerText || el.textContent || "") : ""; -+ chars += code.length; -+ body2 = code.trim() ? keep(fenced(code, langOf(el))) : ""; -+ } else if (tag === "table") { -+ body2 = tableMarkdown(el, next); -+ } else if (tag === "ul" || tag === "ol") { -+ body2 = list(el, next); -+ } else if (tag === "details" && !el.open) { -+ const summary = [...el.children].find((child) => child.localName === "summary"); -+ body2 = summary ? render(summary, next) : ""; -+ } else { -+ body2 = children(el, { ...next, pre: keepsSpace }); -+ } -+ if (found) found.item.name = nameOf(el, oneLine(body2), found.secret); -+ const level = /^h([1-6])$/.exec(tag)?.[1] || (el.getAttribute("role") === "heading" ? el.getAttribute("aria-level") || "2" : ""); -+ if (level) { -+ const title = oneLine(body2); -+ return title ? ` -+ -+${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} -+ -+` : mark; -+ } -+ if (tag === "code" && !ctx.pre) { -+ const code = oneLine(body2); -+ if (!code) return mark; -+ const ticks = code.includes("`") ? "``" : "`"; -+ return `${ticks}${code}${ticks}${mark}`; -+ } -+ if (tag === "blockquote") { -+ const quoted = block(body2); -+ return quoted ? keep(quoted.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n")) : ""; -+ } -+ const end = body2.trimEnd().length; -+ const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -+ if (ctx.pre && !BLOCK_DISPLAY.test(display)) return marked; -+ if (keepsSpace && BLOCK_DISPLAY.test(display) && tag !== "pre") { -+ return keep(expand(marked).replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/^\n+|\s+$/g, ""), ""); -+ } -+ if (BLOCK_DISPLAY.test(display)) return ` -+ -+${marked} -+ -+`; -+ if (INLINE_BLOCK.test(display)) return ` ${marked} `; -+ if (tag === "li") return ` · ${marked}`; -+ return marked; -+ } -+ const body = document.body; -+ let text2 = ""; -+ if (body) { -+ text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); -+ } -+ if (text2.length > limits.chars) { -+ text2 = text2.slice(0, limits.chars); -+ stop ||= `the text passed ${limits.chars} characters`; -+ } -+ const page = { -+ url: location.href, -+ title: document.title, -+ lang: document.documentElement.lang || "", -+ description: document.querySelector('meta[name="description"]')?.content || "", -+ selection: String(getSelection() || "").slice(0, 4e3), -+ format: "markdown", -+ text: text2, -+ items, -+ stats: { ...stats, ms: Math.round(performance.now() - started) } -+ }; -+ if (stop) page.truncated = stop; -+ return page; - } --function ProjectMetrics({ view }) { -- return (0, import_react47.createElement)( -- "section", -- { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, -- view.available || view.reason === null ? null : (0, import_react47.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-metric-cards" }, -- view.cards.map((card) => (0, import_react47.createElement)( -- "div", -- { key: card.key, className: "bt-project-metric", "data-metric": card.key }, -- (0, import_react47.createElement)("span", { className: "bt-project-metric-value" }, card.value), -- (0, import_react47.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), -- (0, import_react47.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) -- )) -- ) -- ); --} --function ProjectRunningTable({ view, onOpen }) { -- if (view.running.length === 0) return (0, import_react47.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react47.createElement)("h4", null, t("Running tasks")), (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); -- return (0, import_react47.createElement)( -- "section", -- { className: "bt-project-running", "data-running": String(view.running.length) }, -- (0, import_react47.createElement)("h4", null, t("Running tasks")), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-running-head" }, -- (0, import_react47.createElement)("span", null, t("Task")), -- (0, import_react47.createElement)("span", null, t("Status")), -- (0, import_react47.createElement)("span", null, t("Who")), -- (0, import_react47.createElement)("span", null, t("Claimed for")), -- (0, import_react47.createElement)("span", null, t("Source")), -- (0, import_react47.createElement)("span", null, t("Last activity")) -- ), -- view.running.map((row) => { -- const open = row.board === null ? void 0 : () => onOpen?.(row.board); -- return (0, import_react47.createElement)( -- "div", -- { -- key: row.key, -- className: "bt-project-running-row", -- "data-task-id": row.id, -- "data-overdue": row.overdue ? "true" : "false", -- role: open === void 0 ? void 0 : "button", -- tabIndex: open === void 0 ? void 0 : 0, -- onClick: open -- }, -- (0, import_react47.createElement)("span", { className: "bt-project-running-title" }, row.title), -- (0, import_react47.createElement)("span", { className: "bt-project-running-status" }, t(row.status)), -- (0, import_react47.createElement)("span", null, row.who ?? METRIC_DASH), -- (0, import_react47.createElement)("span", null, metricText("since", row.sinceMs)), -- (0, import_react47.createElement)("span", null, row.source === null ? METRIC_DASH : t(SOURCE_LABELS[row.source] ?? row.source)), -- (0, import_react47.createElement)("span", null, row.lastActivityAt === null ? METRIC_DASH : new Date(row.lastActivityAt).toLocaleString(dateLocale())) -- ); -- }) -- ); --} --function ProjectQueueGroups({ view, onOpen }) { -- return view.queues.map((entry) => (0, import_react47.createElement)( -- "section", -- { key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group }, -- (0, import_react47.createElement)("h4", null, t(entry.label)), -- entry.items.map((row) => { -- const open = row.board === null ? void 0 : () => onOpen?.(row.board); -- const note = entry.group === "waiting" ? (0, import_react47.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react47.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react47.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react47.createElement)("span", { className: "bt-project-reason" }, row.reason); -- return (0, import_react47.createElement)( -- "div", -- { -- key: row.key, -- className: "bt-project-row", -- "data-task-id": row.id, -- "data-state": row.state ?? void 0, -- role: open === void 0 ? void 0 : "button", -- tabIndex: open === void 0 ? void 0 : 0, -- onClick: open, -- onKeyDown: open === void 0 ? void 0 : (event) => { -- if (event.key === "Enter" || event.key === " ") open(); -- } -- }, -- (0, import_react47.createElement)( -- "span", -- { className: "bt-project-title-wrap" }, -- (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`) -- ), -- note -- ); -- }) -- )); --} --var countText = (value) => value === null ? t("unknown") : String(value); --function ProjectBoardRecord({ record }) { -- const { decisions, meetings } = record; -- const decisionRows = decisions.files.map((file) => (0, import_react47.createElement)( -- "div", -- { key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 }, -- (0, import_react47.createElement)("span", { className: "bt-project-date" }, file.date), -- (0, import_react47.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), -- (0, import_react47.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) -- )); -- const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); -- const proxyText = (meeting) => meeting.participants.filter((participant) => participant.speaksFor !== null).map((participant) => `${participant.name} ${t("for {user}", { user: participant.speaksFor })}`).join(", "); -- const proxyRow = (meeting) => proxyText(meeting) === "" ? null : (0, import_react47.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": meeting.participants.filter((p) => p.speaksFor !== null).map((p) => p.speaksFor).join(", ") }, proxyText(meeting)); -- const levelOf = (meeting) => (0, import_react47.createElement)("span", { className: "bt-project-level", "data-level": meeting.level }, t(meeting.level)); -- const decidedRows = meetings.decided.map((meeting) => (0, import_react47.createElement)( -- "div", -- { key: meeting.key, className: "bt-project-meeting", "data-status": "decided" }, -- (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date), -- (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.question), -- meeting.owner === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`), -- proxyRow(meeting), -- // A decided meeting whose file is gone still reads as decided; the missing path says so -- // instead of the row disappearing with the file. -- meeting.path === "" ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react47.createElement)("span", { className: "bt-project-path" }, meeting.path) -- )); -- const pendingRows = meetings.pending.map((meeting) => (0, import_react47.createElement)( -- "div", -- { key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status, "data-level": meeting.level }, -- (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date), -- (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion), -- levelOf(meeting), -- recipientText(meeting.recipients) === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-recipients" }, recipientText(meeting.recipients)), -- meeting.advance.allowed ? null : (0, import_react47.createElement)("span", { className: "bt-project-paused" }, meeting.advance.reason), -- proxyRow(meeting), -- (0, import_react47.createElement)("span", { className: "bt-project-status" }, meeting.status) -- )); -- return (0, import_react47.createElement)( -- "section", -- { className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) }, -- (0, import_react47.createElement)("h4", null, t("Decisions")), -- decisions.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, -- (0, import_react47.createElement)("h4", null, t("Meetings")), -- meetings.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows, -- meetings.pending.length === 0 ? null : (0, import_react47.createElement)("h5", null, t("Not decided")), -- meetings.pending.length === 0 ? null : pendingRows -- ); -+var readPageScript = (limits = READ_LIMITS) => `(${readPage})(${JSON.stringify(limits)})`; -+var READ_PAGE_SCRIPT = readPageScript(); -+ -+// src/client/bot-browsers.js -+var STORE_KEY = STORAGE.browsers; -+var MAX_LIVE = 6; -+var READ_TIMEOUT_MS = 1e4; -+var PREVIEW_TIMEOUT_MS = 3e3; -+var RESIZE_LATER_MS = 200; -+var SEARCH_URL = "https://www.bing.com/search?q="; -+var PREVIEW_SCRIPT = `(() => { -+ const meta = key => document.querySelector('meta[name="' + key + '"],meta[property="' + key + '"]')?.content ?? '' -+ const paragraph = [...document.querySelectorAll('main p, article p, p')].map(node => node.innerText ?? '').find(text => text.trim().length > 40) ?? '' -+ const text = meta('description') || meta('og:description') || paragraph || document.body?.innerText || '' -+ return text.replace(/\\s+/g, ' ').trim().slice(0, 200) -+})()`; -+var EMPTY = { url: "", title: "", favicon: null, loading: false, canGoBack: false, canGoForward: false, error: null, live: false, excerpt: "" }; -+var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.internal)$/i; -+var BLOCKED = "Cloud metadata addresses do not open here."; -+var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); -+function browserAddress(input) { -+ const text2 = String(input ?? "").trim(); -+ if (text2 === "") return null; -+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; -+ if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; -+ const host = text2.split(/[/?#]/)[0]; -+ const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); -+ const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); -+ if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; -+ return `${SEARCH_URL}${encodeURIComponent(text2)}`; - } --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) { -- const stored = project?.roles !== null && typeof project?.roles === "object" ? project.roles : {}; -- const owner = []; -- const addOwner = (id) => { -- const clean = text(id); -- if (clean !== "" && !owner.includes(clean)) owner.push(clean); -+function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { -+ const views = /* @__PURE__ */ new Map(); -+ const owner = /* @__PURE__ */ new Map(); -+ const tabs = createSource2({}); -+ const pages = createSource2({}); -+ let saved = {}; -+ try { -+ saved = JSON.parse(storage?.getItem(STORE_KEY) ?? "{}") ?? {}; -+ } catch { -+ saved = {}; -+ } -+ let layer; -+ let shown = null; -+ let rect = null; -+ let visible = false; -+ let resizeLater = 0; -+ let disposed = false; -+ let serial = 0; -+ const savedTab = (botId, tabId) => saved[botId]?.tabs?.find((entry) => entry.id === tabId); -+ const publish2 = (tabId, patch) => pages.set((state) => { -+ const botId = owner.get(tabId); -+ return { ...state, [tabId]: { ...EMPTY, ...savedTab(botId, tabId), ...state[tabId], ...patch } }; -+ }); -+ const writeSaved = () => { -+ try { -+ storage?.setItem(STORE_KEY, JSON.stringify(saved)); -+ } catch { -+ } - }; -- addOwner(project?.ownerUserId); -- for (const id of idList(project?.ownerUserIds)) addOwner(id); -- const maintainers = idList(stored.maintainers).filter((id) => !owner.includes(id)); -- const members = idList(stored.members).filter((id) => !owner.includes(id) && !maintainers.includes(id)); -+ const persist = (botId) => { -+ const state = tabs.getSnapshot()[botId]; -+ if (state === void 0) { -+ if (saved[botId] === void 0) return; -+ const { [botId]: _, ...rest } = saved; -+ saved = rest; -+ } else { -+ const snapshot = pages.getSnapshot(); -+ saved = { -+ ...saved, -+ [botId]: { -+ tabs: state.ids.map((id) => ({ id, url: snapshot[id]?.url ?? "", title: snapshot[id]?.title ?? "" })), -+ active: state.active -+ } -+ }; -+ } -+ writeSaved(); -+ }; -+ const ensureTabs = (botId) => { -+ if (tabs.getSnapshot()[botId] !== void 0) return; -+ const entries2 = saved[botId]?.tabs ?? []; -+ const ids = entries2.map((entry) => entry.id); -+ const active = ids.includes(saved[botId]?.active) ? saved[botId].active : ids.at(-1) ?? null; -+ for (const entry of entries2) owner.set(entry.id, botId); -+ tabs.set((state) => ({ ...state, [botId]: { ids, active } })); -+ for (const entry of entries2) publish2(entry.id, { url: entry.url ?? "", title: entry.title ?? "", live: false }); -+ }; -+ const ensureLayer = () => { -+ if (layer?.isConnected) return layer; -+ layer = doc.createElement("div"); -+ layer.className = "bt-browser-layer"; -+ Object.assign(layer.style, { position: "fixed", inset: "0", zIndex: "41", pointerEvents: "none" }); -+ doc.body.append(layer); -+ return layer; -+ }; -+ const place = (entry) => { -+ const box = rect ?? { left: 0, top: 0, width: 800, height: 600 }; -+ Object.assign(entry.element.style, { -+ left: `${box.left}px`, -+ top: `${box.top}px`, -+ width: `${box.width}px`, -+ height: `${box.height}px`, -+ visibility: shown === entry.tabId && visible && rect ? "visible" : "hidden" -+ }); -+ }; -+ async function preview(tabId) { -+ const entry = views.get(tabId); -+ if (entry === void 0 || !entry.ready || !/^https?:/i.test(entry.element.getURL())) return false; -+ let timer; -+ const timeout = new Promise((resolve) => { -+ timer = setTimeout(() => resolve(""), PREVIEW_TIMEOUT_MS); -+ }); -+ try { -+ const excerpt = await Promise.race([entry.element.executeJavaScript(PREVIEW_SCRIPT), timeout]); -+ if (typeof excerpt !== "string" || excerpt === "" || views.get(tabId) !== entry) return false; -+ publish2(tabId, { excerpt }); -+ return true; -+ } catch { -+ return false; -+ } finally { -+ clearTimeout(timer); -+ } -+ } -+ const observe = (entry) => { -+ const { element } = entry; -+ if (!entry.ready) return; -+ const url = element.getURL(); -+ if (url.startsWith("about:blank#")) return; -+ if (entry.fresh) { -+ element.clearHistory(); -+ entry.fresh = false; -+ } -+ const title = element.getTitle(); -+ publish2(entry.tabId, { url, title, loading: element.isLoading(), canGoBack: element.canGoBack(), canGoForward: element.canGoForward(), live: true }); -+ persist(entry.botId); -+ }; -+ async function create(tabId) { -+ const botId = owner.get(tabId); -+ const reservation = await bridge.acquire(`bot:${botId}`); -+ if (disposed) { -+ await bridge.release(reservation.lease).catch(() => { -+ }); -+ return void 0; -+ } -+ const element = doc.createElement("webview"); -+ element.dataset.sidebarBrowserFrame = "webview"; -+ Object.assign(element.style, { position: "fixed", display: "flex", border: "0", pointerEvents: "auto", background: "#fff" }); -+ element.dataset.btBrowser = tabId; -+ element.setAttribute("name", reservation.lease); -+ element.setAttribute("partition", reservation.partition); -+ element.setAttribute("allowpopups", ""); -+ element.setAttribute("src", `about:blank#${reservation.lease}`); -+ const entry = { botId, tabId, element, lease: reservation.lease, ready: false, fresh: true, pending: pages.getSnapshot()[tabId]?.url || savedTab(botId, tabId)?.url, usedAt: Date.now() }; -+ const lifetime = new AbortController(); -+ entry.stop = () => lifetime.abort(); -+ const { signal } = lifetime; -+ const unsubscribe = bridge.onOpenRequested(reservation.lease, (url) => void openTab(botId, url)); -+ signal.addEventListener("abort", unsubscribe, { once: true }); -+ element.addEventListener("dom-ready", () => { -+ entry.ready = true; -+ observe(entry); -+ if (entry.pending) { -+ const url = entry.pending; -+ entry.pending = void 0; -+ void element.loadURL(url).catch(() => { -+ }); -+ } -+ }, { signal }); -+ for (const name of ["did-navigate", "did-navigate-in-page", "did-start-loading", "did-stop-loading", "page-title-updated"]) { -+ element.addEventListener(name, () => observe(entry), { signal }); -+ } -+ element.addEventListener("did-stop-loading", () => { -+ void preview(tabId); -+ }, { signal }); -+ element.addEventListener("page-favicon-updated", (event) => { -+ const favicon = event.favicons?.[0]; -+ if (typeof favicon === "string" && favicon.startsWith("data:")) publish2(tabId, { favicon }); -+ }, { signal }); -+ element.addEventListener("did-start-navigation", (event) => { -+ if (!event.isMainFrame) return; -+ if (blockedAddress(event.url)) { -+ element.stop(); -+ publish2(tabId, { loading: false, error: BLOCKED }); -+ return; -+ } -+ publish2(tabId, { loading: true, error: null }); -+ }, { signal }); -+ element.addEventListener("did-fail-load", (event) => { -+ if (event.isMainFrame && event.errorCode !== -3) publish2(tabId, { loading: false, error: event.errorDescription || `Error ${event.errorCode}` }); -+ }, { signal }); -+ element.addEventListener("render-process-gone", () => { -+ if (views.get(tabId) === void 0) return; -+ void release(tabId); -+ publish2(tabId, { loading: false, error: "The page crashed." }); -+ }, { signal }); -+ views.set(tabId, entry); -+ ensureLayer().append(element); -+ place(entry); -+ publish2(tabId, { live: true, loading: Boolean(entry.pending) }); -+ return entry; -+ } -+ const opening = /* @__PURE__ */ new Map(); -+ function open(tabId) { -+ const entry = views.get(tabId); -+ if (entry) return Promise.resolve(entry); -+ let job = opening.get(tabId); -+ if (job === void 0) { -+ job = create(tabId).catch((error) => { -+ console.warn("[ds-bot] browser acquire", error); -+ publish2(tabId, { loading: false, error: String(error?.message ?? error) }); -+ return void 0; -+ }).finally(() => opening.delete(tabId)); -+ opening.set(tabId, job); -+ void job.then(() => trim()); -+ } -+ return job; -+ } -+ function trim() { -+ const idle = [...views.values()].filter((entry) => entry.tabId !== shown).sort((a, b) => a.usedAt - b.usedAt); -+ while (views.size > MAX_LIVE && idle.length > 0) void release(idle.shift().tabId); -+ } -+ async function release(tabId) { -+ const entry = views.get(tabId); -+ if (entry === void 0) return; -+ views.delete(tabId); -+ entry.stop(); -+ entry.element.remove(); -+ publish2(tabId, { live: false, loading: false }); -+ await bridge.release(entry.lease).catch((error) => console.warn("[ds-bot] browser release", error)); -+ } -+ function openTab(botId, url) { -+ ensureTabs(botId); -+ const tabId = `t${Date.now().toString(36)}-${++serial}`; -+ owner.set(tabId, botId); -+ tabs.set((state) => { -+ const own = state[botId] ?? { ids: [], active: null }; -+ return { ...state, [botId]: { ids: [...own.ids, tabId], active: tabId } }; -+ }); -+ publish2(tabId, { url: "", title: "", live: false }); -+ if (typeof url === "string" && url !== "") void navigate(tabId, url); -+ persist(botId); -+ return tabId; -+ } -+ async function closeTab(botId, tabId) { -+ const state = tabs.getSnapshot()[botId]; -+ if (state === void 0 || !state.ids.includes(tabId)) return; -+ await opening.get(tabId)?.catch(() => { -+ }); -+ if (shown === tabId) await show(null); -+ await release(tabId); -+ tabs.set((snapshot) => { -+ const current = snapshot[botId]; -+ if (current === void 0 || !current.ids.includes(tabId)) return snapshot; -+ const at = current.ids.indexOf(tabId); -+ const ids = current.ids.filter((id) => id !== tabId); -+ const active = current.active === tabId ? ids[Math.min(at, ids.length - 1)] ?? null : current.active; -+ const next = { ...snapshot }; -+ if (ids.length === 0) delete next[botId]; -+ else next[botId] = { ids, active }; -+ return next; -+ }); -+ owner.delete(tabId); -+ pages.set((snapshot) => { -+ if (snapshot[tabId] === void 0) return snapshot; -+ const { [tabId]: _, ...rest } = snapshot; -+ return rest; -+ }); -+ persist(botId); -+ } -+ function activate(botId, tabId) { -+ ensureTabs(botId); -+ const state = tabs.getSnapshot()[botId]; -+ if (state === void 0 || !state.ids.includes(tabId) || state.active === tabId) return; -+ tabs.set((snapshot) => ({ ...snapshot, [botId]: { ...state, active: tabId } })); -+ persist(botId); -+ } -+ const activeTab = (botId) => tabs.getSnapshot()[botId]?.active ?? null; -+ async function show(tabId, box = null) { -+ if (tabId !== null && tabId === shown && visible && box !== null) { -+ rect = box; -+ const entry2 = views.get(tabId); -+ if (entry2) place(entry2); -+ clearTimeout(resizeLater); -+ resizeLater = setTimeout(() => { -+ for (const other of views.values()) place(other); -+ }, RESIZE_LATER_MS); -+ return; -+ } -+ shown = tabId; -+ if (box) rect = box; -+ visible = tabId !== null && box !== null; -+ for (const entry2 of views.values()) place(entry2); -+ if (tabId === null) return; -+ const entry = await open(tabId); -+ if (entry === void 0 || shown !== tabId) return; -+ entry.usedAt = Date.now(); -+ place(entry); -+ } -+ async function navigate(tabId, input) { -+ const url = browserAddress(input); -+ if (url === null || blockedAddress(url)) { -+ publish2(tabId, { error: url === null ? "Only http and https addresses open here." : BLOCKED }); -+ return false; -+ } -+ const entry = await open(tabId); -+ if (entry === void 0) return false; -+ publish2(tabId, { url, loading: true, error: null }); -+ persist(owner.get(tabId)); -+ if (!entry.ready) entry.pending = url; -+ else void entry.element.loadURL(url).catch(() => { -+ }); -+ return true; -+ } -+ async function forget(botId) { -+ ensureTabs(botId); -+ for (const tabId of tabs.getSnapshot()[botId]?.ids ?? []) { -+ await opening.get(tabId)?.catch(() => { -+ }); -+ if (shown === tabId) await show(null); -+ await release(tabId); -+ owner.delete(tabId); -+ pages.set((snapshot) => { -+ if (snapshot[tabId] === void 0) return snapshot; -+ const { [tabId]: _, ...rest } = snapshot; -+ return rest; -+ }); -+ } -+ tabs.set((snapshot) => { -+ if (snapshot[botId] === void 0) return snapshot; -+ const { [botId]: _, ...rest } = snapshot; -+ return rest; -+ }); -+ if (saved[botId] !== void 0) { -+ const { [botId]: _, ...rest } = saved; -+ saved = rest; -+ writeSaved(); -+ } -+ } - return { -- chief: botIdOf(project?.chief), -- agent: botIdOf(project?.assistant), -- roles: { owner, maintainers, members }, -- empty: owner.length === 0 && maintainers.length === 0 && members.length === 0, -- // Both come from the `projects` listing (#1828), which the overlay keeps on the record: the -- // `project-read` record answers no rank, and the panel never re-derives the host's policy. -- role: text(project?.role), -- choices: idList(project?.choices) -- }; --} --var teamRow = (role, label, botId, onOpenBot) => (0, import_react47.createElement)( -- "div", -- { -- key: role, -- className: "bt-project-team-row", -- "data-team-role": role, -- // A row with no Bot is the fallback the slice specifies: still drawn, with nothing to open. -- ...botId === "" ? { "data-team-state": "not-set" } : { -- "data-open-bot": botId, -- role: "button", -- tabIndex: 0, -- "aria-label": t("Open {name}'s chat", { name: botId }), -- onClick: () => onOpenBot?.(botId), -- onKeyDown: (event) => { -- if (event.key === "Enter" || event.key === " ") { -- event.preventDefault(); -- onOpenBot?.(botId); -- } -+ tabs, -+ pages, -+ /** Restore a Bot's saved strip (or an empty one) so the panel can render it. */ -+ ensureTabs, -+ openTab, -+ closeTab, -+ activate, -+ preview, -+ show, -+ navigate, -+ command(tabId, name) { -+ const entry = views.get(tabId); -+ if (!entry?.ready) return; -+ if (name === "back" && entry.element.canGoBack()) entry.element.goBack(); -+ else if (name === "forward" && entry.element.canGoForward()) entry.element.goForward(); -+ else if (name === "reload") entry.element.reload(); -+ else if (name === "stop") entry.element.stop(); -+ }, -+ focus(tabId) { -+ views.get(tabId)?.element.focus(); -+ }, -+ /** Pages a read can reach: each Bot's active tab, live and showing a document. */ -+ openPages() { -+ const open2 = {}; -+ const snapshot = pages.getSnapshot(); -+ for (const [botId, state] of Object.entries(tabs.getSnapshot())) { -+ const tabId = state.active; -+ const entry = tabId ? views.get(tabId) : void 0; -+ const page = tabId ? snapshot[tabId] : void 0; -+ if (entry?.ready && page?.url) open2[botId] = { url: page.url, title: page.title }; - } -+ return open2; -+ }, -+ /** A Bot's read reaches its active tab. */ -+ async read(botId) { -+ const tabId = activeTab(botId); -+ const entry = tabId ? views.get(tabId) : void 0; -+ if (!entry?.ready) throw new Error("This Bot has no browser page open."); -+ let timer; -+ const timeout = new Promise((_, reject) => { -+ timer = setTimeout(() => reject(new Error("The page took too long to read.")), READ_TIMEOUT_MS); -+ }); -+ try { -+ return await Promise.race([entry.element.executeJavaScript(READ_PAGE_SCRIPT), timeout]); -+ } finally { -+ clearTimeout(timer); -+ } -+ }, -+ noteRead(botId) { -+ const tabId = activeTab(botId); -+ if (tabId) publish2(tabId, { readAt: Date.now() }); -+ }, -+ release, -+ forget, -+ /** Forget every Bot outside `botIds`, as when Bots were deleted from another window. */ -+ retain(botIds) { -+ const keep = new Set(botIds); -+ const known = /* @__PURE__ */ new Set([...Object.keys(tabs.getSnapshot()), ...Object.keys(saved), ...owner.values()]); -+ for (const entry of views.values()) known.add(entry.botId); -+ return Promise.all([...known].filter((botId) => !keep.has(botId)).map(forget)); -+ }, -+ dispose() { -+ disposed = true; -+ clearTimeout(resizeLater); -+ for (const entry of views.values()) void release(entry.tabId); -+ layer?.remove(); - } -- }, -- (0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), -- botId === "" ? (0, import_react47.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react47.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`) --); --var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]]; --function ProjectTeam({ project, onOpenBot }) { -- const view = projectTeamView(project); -- const ranks = view.empty ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react47.createElement)( -- "span", -- { className: "bt-project-team-roles" }, -- POOL_RANKS.map(([role, key]) => view.roles[key].length === 0 ? null : (0, import_react47.createElement)( -- "span", -- { key: role, className: "bt-project-team-role", "data-pool-role": role }, -- `${roleText(role)}: ${view.roles[key].map((id) => `@${id}`).join(", ")}` -- )) -- ); -- return (0, import_react47.createElement)( -- "section", -- { className: "bt-project-team", "data-team": "chief,agent,pool" }, -- (0, import_react47.createElement)("h4", null, t("Project team")), -- teamRow("chief", t("Project Chief"), view.chief, onOpenBot), -- teamRow("agent", t("My agent"), view.agent, onOpenBot), -- (0, import_react47.createElement)( -- "div", -- { key: "pool", className: "bt-project-team-row", "data-team-role": "pool" }, -- (0, import_react47.createElement)("span", { key: "label", className: "bt-project-team-label" }, t("Role pool")), -- ranks, -- // The caller's own rank and what the host said they may switch to: the same answer the -- // sidebar menu reads, printed where the pool is. -- view.role === "" ? null : (0, import_react47.createElement)( -- "span", -- { key: "mine", className: "bt-project-team-myrole", "data-team-my-role": view.role }, -- [ -- t("My role"), -- ": ", -- roleText(view.role), -- view.choices.length === 0 ? null : ` · ${t("Can switch to {roles}", { roles: view.choices.map(roleText).join(", ") })}` -- ] -- ) -- ) -- ); --} --function ProjectBoardPane({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics, onOpen, onSettings, onMembers, onOpenBot }) { -- const view = projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics }); -- return (0, import_react47.createElement)( -- "div", -- { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, -- (0, import_react47.createElement)("h3", null, view.title), -- (0, import_react47.createElement)(ProjectTeam, { project, onOpenBot }), -- // Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown. -- view.counts === null ? null : (0, import_react47.createElement)( -- "div", -- { className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" }, -- (0, import_react47.createElement)("span", null, [t("Waiting"), ": ", (0, import_react47.createElement)("b", { key: "w" }, countText(view.counts.waiting))]), -- (0, import_react47.createElement)("span", null, [t("In flight"), ": ", (0, import_react47.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), -- (0, import_react47.createElement)("span", null, [t("Blocked"), ": ", (0, import_react47.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), -- (0, import_react47.createElement)("span", null, [t("Done"), ": ", (0, import_react47.createElement)("b", { key: "d" }, countText(view.counts.done))]) -- ), -- // #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_react47.createElement)( -- "div", -- { className: "bt-project-actions" }, -- (0, import_react47.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), -- (0, import_react47.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) -- ), -- (0, import_react47.createElement)(ProjectMetrics, { view: view.metrics }), -- (0, import_react47.createElement)(ProjectRunningTable, { view: view.metrics, onOpen }), -- (0, import_react47.createElement)(ProjectBoardRows, { view, onOpen }), -- (0, import_react47.createElement)(ProjectQueueGroups, { view, onOpen }), -- (0, import_react47.createElement)(ProjectDoneGroup, { view, onOpen }), -- (0, import_react47.createElement)(ProjectBoardRecord, { record: view.record }) -- ); --} --function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { -- const save = request ?? call; -- const [kind, setKind] = (0, import_react47.useState)("bot"); -- const [memberId, setMemberId] = (0, import_react47.useState)(""); -- const [note, setNote] = (0, import_react47.useState)(""); -- const [assistantOff, setAssistantOff] = (0, import_react47.useState)(assistantMine === void 0 ? project?.assistant?.off === true : assistantMine?.off === true); -- const [status, setStatus] = (0, import_react47.useState)(""); -- const id = text(project?.id); -- const run = async (endpoint, payload, done) => { -- try { -- const result = await save(endpoint, payload); -- setStatus(done); -- onSaved?.(result); -- return result; -- } catch (error) { -- setStatus(hostText(error?.message ?? String(error))); -- return null; -- } -- }; -- return (0, import_react47.createElement)( -- "div", -- { className: "bt-project-panel", "data-panel": "project-settings" }, -- (0, import_react47.createElement)("h3", null, t("Project settings")), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-field" }, -- (0, import_react47.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-row-actions" }, -- (0, import_react47.createElement)( -- "select", -- { "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) }, -- (0, import_react47.createElement)("option", { value: "bot" }, t("Bot")), -- (0, import_react47.createElement)("option", { value: "group" }, t("Group")) -- ), -- (0, import_react47.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }), -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")), -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind")) -- ) -- ), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-field" }, -- (0, import_react47.createElement)("label", null, t("Project Assistant")), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-row-actions" }, -- (0, import_react47.createElement)("label", null, (0, import_react47.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")), -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save")) -- ) -- ), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-field" }, -- // The host knows two memory kinds; the free text the panel takes is recorded as a decision, -- // never as a third kind the host would refuse (#1820). -- (0, import_react47.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")), -- (0, import_react47.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-row-actions" }, -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory")) -- ) -- ), -- (0, import_react47.createElement)("div", { className: "bt-project-status-line", role: "status" }, status), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-row-actions" }, -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -- ) -- ); --} --function ProjectMembersPanel({ project, rows, onClose } = {}) { -- const owners = []; -- 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 = []; -- for (const row of Array.isArray(rows) ? rows : []) { -- const name = text(row?.assignee); -- if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name); -- } -- const chief = text(project?.chief?.botId); -- return (0, import_react47.createElement)( -- "div", -- { className: "bt-project-panel", "data-panel": "project-members" }, -- (0, import_react47.createElement)("h3", null, t("Members")), -- (0, import_react47.createElement)( -- "ul", -- { className: "bt-project-members" }, -- owners.map((owner) => (0, import_react47.createElement)( -- "li", -- { key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner }, -- (0, import_react47.createElement)("span", null, `@${owner}`), -- (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Owner")) -- )), -- chief === "" ? null : (0, import_react47.createElement)( -- "li", -- { key: `chief:${chief}`, className: "bt-project-member", "data-member-role": "chief", "data-member-id": chief }, -- (0, import_react47.createElement)("span", null, t("Project Chief")), -- (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Chief of this Project")) -- ), -- boardMembers.map((name) => (0, import_react47.createElement)( -- "li", -- { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, -- (0, import_react47.createElement)("span", null, `@${name}`), -- (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) -- )) -- ), -- owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, -- (0, import_react47.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.")), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-row-actions" }, -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) -- ) -- ); - } --var PROJECT_BOARD_POLL_MS = 5e3; --var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project"); --function ProjectBoardOverlay({ actions, call: request } = {}) { -- const read = request ?? call; -- const [state, setState] = (0, import_react47.useState)(null); -- const [panel, setPanel] = (0, import_react47.useState)(null); -- const [hash, setHash] = (0, import_react47.useState)(() => typeof window === "undefined" ? "" : window.location.hash); -- const close = (0, import_react47.useCallback)(() => { -- if (typeof window !== "undefined") window.location.hash = ""; -- setPanel(null); -- setState(null); -- }, []); -- (0, import_react47.useEffect)(() => { -- if (typeof window === "undefined") return void 0; -- const onKey = (event) => { -- if (event.key !== "Escape") return; -- if (panel !== null) setPanel(null); -- else close(); -- }; -- window.addEventListener("keydown", onKey); -- return () => window.removeEventListener("keydown", onKey); -- }, [panel, close]); -- (0, import_react47.useEffect)(() => { -- if (typeof window === "undefined") return void 0; -- const sync = () => setHash(window.location.hash); -- window.addEventListener("hashchange", sync); -- sync(); -- return () => window.removeEventListener("hashchange", sync); -- }, []); -- (0, import_react47.useEffect)(() => { -- if (typeof window === "undefined") return void 0; -- if (!hash.startsWith("#project")) { -- setState(null); -- return void 0; -+var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -+function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.randomUUID(), retryMs = 1e3 }) { -+ let stopped = false; -+ let poll; -+ let reported = ""; -+ const answered = /* @__PURE__ */ new Map(); -+ const answer = async ({ id, botId }) => { -+ let job = answered.get(id); -+ if (job === void 0) { -+ job = (async () => { -+ try { -+ const result = { id, ok: true, page: await browsers.read(botId) }; -+ browsers.noteRead(botId); -+ return result; -+ } catch (error) { -+ return { id, ok: false, error: error instanceof Error ? error.message : String(error) }; -+ } -+ })(); -+ answered.set(id, job); -+ if (answered.size > 200) answered.delete(answered.keys().next().value); - } -- const wanted = hash.slice("#project".length).replace(/^=/, ""); -- let gone = false; -- const loadBoard = async () => { -- const listing = await read("projects"); -- const projects = listing?.projects ?? []; -- const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); -- if (project === void 0) return { project: null, rows: [], canManage: false, tasks: [] }; -- const [record, board, taskList] = await Promise.all([ -- read("project-read", { id: project.id }), -- read("project-board", { id: project.id, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), -- read("tasks", {}) -- ]); -- const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; -- 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 { project: withRole, assistantMine: record?.assistantMine ?? null, rows: board?.tasks ?? [], done: board?.done ?? [], canManage: record?.canManage === true, tasks, decisions: board?.decisions, meetings: board?.meetings, queues, metrics: board?.metrics ?? null }; -- }; -- const refresh = async (first) => { -+ await post("browser-result", await job).catch((error) => console.warn("[ds-bot] browser result", error)); -+ }; -+ const loop = async () => { -+ let failures = 0; -+ while (!stopped) { -+ poll = new AbortController(); -+ const open = browsers.openPages(); -+ reported = JSON.stringify(open); - try { -- const next = await loadBoard(); -- if (!gone) setState(next); -+ const requests = await post("browser-wait", { clientId, open }, poll.signal); -+ failures = 0; -+ for (const request of Array.isArray(requests) ? requests : []) void answer(request); - } catch { -- if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); -+ if (stopped) return; -+ if (poll.signal.aborted) continue; -+ failures += 1; -+ await sleep(Math.min(3e4, retryMs * 2 ** Math.min(failures, 5))); - } -- }; -- void refresh(true); -- const timer = setInterval(() => { -- if (!boardPollPaused()) void refresh(false); -- }, PROJECT_BOARD_POLL_MS); -- return () => { -- gone = true; -- clearInterval(timer); -- }; -- }, [hash]); -- if (state === null) return null; -- return (0, import_react47.createElement)( -- "div", -- { className: "bt-project-overlay" }, -- (0, import_react47.createElement)("div", { className: "bt-scrim", onClick: close }), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") }, -- (0, import_react47.createElement)( -- "div", -- { className: "bt-mem-head" }, -- (0, import_react47.createElement)("h2", {}, t("Project board")), -- (0, import_react47.createElement)("span", { className: "bt-mem-head-gap" }), -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close")) -- ), -- (0, import_react47.createElement)( -- "div", -- { className: "bt-project-body" }, -- state.project === null ? (0, import_react47.createElement)( -- "div", -- { className: "bt-project-empty" }, -- (0, import_react47.createElement)("p", null, t("No Project yet — create one.")), -- (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -- close(); -- actions?.openSettings?.("projects"); -- } }, t("Open Projects in Settings")) -- ) : panel === "settings" ? (0, import_react47.createElement)(ProjectSettingsPanel, { project: state.project, assistantMine: state.assistantMine, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react47.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react47.createElement)(ProjectBoardPane, { -- project: state.project, -- rows: state.rows, -- done: state.done, -- canManage: state.canManage, -- tasks: state.tasks, -- decisions: state.decisions, -- meetings: state.meetings, -- queues: state.queues, -- metrics: state.metrics, -- onSettings: () => setPanel("settings"), -- onMembers: () => setPanel("members"), -- // #1860: a team row opens that Bot's chat. The board is a modal over the conversation, -- // so it closes first — otherwise the chat opens behind it. -- onOpenBot: (id) => { -- close(); -- actions?.openSession?.(id); -- }, -- // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and -- // lands on that Bot or group. Nothing here re-decides which board that is. -- onOpen: (board) => { -- if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`; -- actions?.openBoard?.(); -- setState(null); -- } -- }) -- ) -- ) -- ); -+ } -+ }; -+ const offPages = browsers.pages.subscribe(() => { -+ if (JSON.stringify(browsers.openPages()) !== reported) poll?.abort(); -+ }); -+ void loop(); -+ return () => { -+ stopped = true; -+ offPages(); -+ poll?.abort(); -+ }; -+} -+ -+// src/client/mobile.js -+var MOBILE_MAX_WIDTH = 640; -+var MOBILE_CSS = ` -+@media (max-width:${MOBILE_MAX_WIDTH}px){ -+.bt-held-actions{max-width:100%} -+.bt-held-card{padding:10px 12px} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600} -+:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default} -+.bt-held-retry{min-height:32px} -+.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))} -+.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)} -+.bt-flybot-pointer{max-width:100%;min-width:0} -+.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none} -+.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} -+.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px} -+.bt-inbox-card{flex-wrap:wrap;row-gap:8px} -+.bt-inbox-copy{flex-basis:calc(100% - 38px)} -+/* In progress and Done only. A Needs you ask is the whole text: inbox.js unsets this clamp on those five statuses (#1177). */ -+.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2} -+.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px} -+.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px} -+} -+`; -+ -+// src/client/panes.js -+var PANE_MIN_WIDTH_CSS = ` -+.bt-panel{min-width:0} -+.bt-panel-view{min-width:0} -+.bt-drawer-body{min-width:0;overflow-wrap:anywhere} -+.bt-drawer-body :is(.bt-muted,.bt-note,.bt-section-title,.bt-drawer-role,.bt-drawer-permission){min-width:0;overflow-wrap:anywhere} -+.bt-panel :is(.bt-member,.bt-members,.bt-bcard-row,.bt-drawer-model,.bt-usage-row){min-width:0} -+.bt-panel :is(.bt-muted,.bt-note){white-space:normal} -+[class*="_centerCol"]{min-width:0} -+html,body,#root{overflow-x:clip} -+[class*="_frame"]>[class*="_centerCol"]{overflow-x:clip} -+[class*="_frame"]:has(> [class*="_centerCol"]){overflow-x:clip} -+`; -+var COMPOSER = "[data-composer-input], [data-composer-card]"; -+function clipScroll(el) { -+ if (el && el.scrollLeft > 0) el.scrollLeft = 0; -+} -+function appFrameOf(doc) { -+ if (typeof doc.querySelector !== "function") return null; -+ const col = doc.querySelector('[class*="_centerCol"]'); -+ const frame = col?.parentElement; -+ if (!frame) return null; -+ const name = frame.className; -+ const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; -+ return text2.includes("_frame") ? frame : null; -+} -+function levelShell(doc, from) { -+ const page = doc.scrollingElement || doc.documentElement; -+ clipScroll(page); -+ clipScroll(doc.documentElement); -+ clipScroll(doc.body); -+ clipScroll(appFrameOf(doc)); -+ for (let at = from?.parentElement || null; at; at = at.parentElement) clipScroll(at); -+} -+function composerOf(node) { -+ let el = node; -+ if (el && el.nodeType !== 1) el = el.parentElement; -+ if (!el || typeof el.closest !== "function") return null; -+ return el.closest(COMPOSER); -+} -+function watchShellLevel(doc) { -+ const later = (from) => { -+ const reset = () => levelShell(doc, from); -+ reset(); -+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); -+ }; -+ const onFocus = (event) => later(composerOf(event.target) || void 0); -+ const onInput = (event) => { -+ const composer = composerOf(event.target); -+ if (composer) later(composer); -+ }; -+ const onScroll = (event) => { -+ const target = event.target; -+ if (target === doc || target === doc.documentElement || target === doc.body) levelShell(doc); -+ }; -+ doc.addEventListener("focusin", onFocus); -+ doc.addEventListener("input", onInput); -+ doc.addEventListener("scroll", onScroll, true); -+ return () => { -+ doc.removeEventListener("focusin", onFocus); -+ doc.removeEventListener("input", onInput); -+ doc.removeEventListener("scroll", onScroll, true); -+ }; - } - - // src/client/friend-lines.js -diff --git a/index.js b/index.js ---- a/index.js -+++ b/index.js -@@ -42,6 +42,7 @@ import * as capabilities from './src/host/capabilities.js' - import * as schedules from './src/host/schedules.js' - import * as projectAssistant from './src/host/project-assistant.js' - import * as projectChief from './src/host/project-chief.js' -+import * as projectTeam from './src/host/project-team.js' - import * as automationDefault from './src/host/automation-default.js' - import * as usage from './src/host/usage.js' - import * as update from './src/host/update.js' -@@ -60,7 +60,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses - // the functions other modules call; they call them through `rt` at run time, never during - // install. Listeners run and tools are listed in registration order, so this order holds: - // usage meters model requests after every listener that rewrites them. --const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel] -+const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, projectTeam, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, channel] - - export function apply(ctx, config = {}) { - const rt = createRuntime(ctx, config) -diff --git a/src/client/header.js b/src/client/header.js ---- a/src/client/header.js -+++ b/src/client/header.js -@@ -7,6 +7,8 @@ import { clockLabel, movedOnSince, openingTurn, unseenSince } from './cells.js' - import { ActivityIndicator } from './activity-row.js' - import { BusyAckBubble } from './busy-ack.js' - import { t } from './i18n.js' -+import { call } from './api.js' -+import { ProjectAgentEntry, projectAgentFor } from './project-board.js' - import { friendBotTitle, roomTitle } from '../shared/remote-label.js' - - // ------------------------------------------------------------------------- -@@ -69,6 +69,9 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus - const owner = roster.byId[sessionId]?.id ?? sessionId - const doing = useActivity(map => map[owner]) - const bot = roster.byId[sessionId] -+ // #1864: an owner's Bot that belongs to a Project gets the entry to that Project's assistant in -+ // its own chat header. The roster already carries the binding, so nothing else is read here. -+ const projectAgent = projectAgentFor(bot, roster.self?.memberId) - const room = roster.roomsById[sessionId] - const detailsOpen = useUi(state => state.details === sessionId) - const full = bot?.remote -@@ -96,6 +96,9 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus - h(MemoryNotice, { shown: memoryNotice }), - h('span', { className: 'bt-pill-go', 'aria-hidden': true }, h(GoArrow))), - h('span', { className: 'bt-sr', role: 'status' }, memoryNotice ? t('Memory updated') : ''), -+ projectAgent === null ? null : h(ProjectAgentEntry, { -+ projectId: projectAgent.projectId, read: call, onOpen: id => actions?.openSession?.(id), -+ }), - h(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), - h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), - h(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })) -diff --git a/src/client/i18n.js b/src/client/i18n.js ---- a/src/client/i18n.js -+++ b/src/client/i18n.js -@@ -1403,6 +1403,15 @@ const ZH = { - 'Role pool': '角色池', - 'Can switch to {roles}': '可切换为 {roles}', - 'No one in this pool yet.': '角色池里还没有人。', -+ // #1864: the Project team's dossier — persona, memory and skills are read-only in this slice. -+ 'Dossier': '档案', -+ 'Persona': '人设', -+ 'Skills': '技能', -+ 'No memory yet.': '还没有记忆。', -+ 'No skills attached.': '还没有附加技能。', -+ 'Open chat': '打开对话', -+ 'Open my agent': '打开我的项目助手', -+ "Open {name}'s dossier": '打开 {name} 的档案', - 'No cards yet.': '还没有卡片。', - 'Open Projects in Settings': '在设置中打开项目', - } -diff --git a/src/host/channel.js b/src/host/channel.js ---- a/src/host/channel.js -+++ b/src/host/channel.js -@@ -675,6 +675,7 @@ export function install(rt) { - case 'project-bind': - case 'project-of': - case 'project-board': -+ case 'project-team': - case 'project-role': - case 'project-delete': - case 'project-memory': -diff --git a/src/host/memory.js b/src/host/memory.js ---- a/src/host/memory.js -+++ b/src/host/memory.js -@@ -411,6 +411,7 @@ export function install(rt) { - memoryEndpoint: async () => { throw new Error('Memory is turned off on this host') }, - memoryNews: () => ({}), - memoryReviews: () => 0, -+ memoryDigest: () => ({ head: '', topics: [] }), - }) - return - } -@@ -1469,9 +1469,22 @@ export function install(rt) { - }) - } - -+ // The Project board's dossier (#1864) reads the head and the topic names only: never an entry -+ // body from a topic, and never another scope. It is a read seam, so the panel adds no second -+ // memory file reader beside this one and cannot drift from where `remember` writes. -+ function memoryDigest(botId, limits = {}) { -+ const scope = ownScope(String(botId ?? '')) -+ const head = Number.isFinite(limits.head) ? limits.head : 4000 -+ const topics = Number.isFinite(limits.topics) ? limits.topics : 64 -+ return { -+ head: readText(join(dirOf(scope), MAIN)).slice(0, head), -+ topics: topicNames(scope).slice(0, topics), -+ } -+ } -+ - Object.assign(rt, { - memoryEnabled: true, renewMemory, archiveBotMemory, memoryEndpoint, memoryReviews, - memoryNews: () => Object.fromEntries(news), -- botMemoryRead, botMemoryAppend, -+ botMemoryRead, botMemoryAppend, memoryDigest, - }) - } -diff --git a/src/host/store.js b/src/host/store.js ---- a/src/host/store.js -+++ b/src/host/store.js -@@ -719,6 +719,10 @@ export function install(rt) { - return { project: readProject(project.id), ...(result ?? {}) } - } - case 'project-of': return { project: projectOfMember(projectText(body.kind, 20), projectText(body.id, 200)) } -+ // The read-only team dossier (#1864): the Project's Chief, its assistant and its pool, each -+ // with persona, memory head/topics and skills. The read lives in project-team.js; a missing -+ // Bot is a `null` row there, never an error for the whole answer. -+ case 'project-team': return rt.projectTeam?.(projectText(body.id, 200)) ?? { rows: [] } - // The read-only board (#1696): Project id in, its tasks out. The aggregate lives in - // project-board.js; it reads and returns, and writes nothing. The actor goes with the id - // so the board applies the same visibility rule `tasks` does (#1723). diff --git a/patches/ds-bot/files/src/client/project-board.js b/patches/ds-bot/files/src/client/project-board.js index 73fdd5649..41a062d6a 100644 --- a/patches/ds-bot/files/src/client/project-board.js +++ b/patches/ds-bot/files/src/client/project-board.js @@ -81,6 +81,10 @@ export const PROJECT_BOARD_CSS = ` .bt-project-field label{font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} .bt-project-field input,.bt-project-field select,.bt-project-field textarea{font:inherit;padding:6px 8px;border-radius:8px;border:1px solid var(--bt-line,rgba(127,127,127,.32));background:var(--bt-card,rgba(127,127,127,.08));color:inherit} .bt-project-field textarea{min-height:88px;resize:vertical} +/* #1887: the counter under the memory box. It stays quiet until the text passes the bound, and + past it turns the one colour that says so; the disabled Save is the state a reader acts on. */ +.bt-project-count{font-size:11px;opacity:.6;font-variant-numeric:tabular-nums} +.bt-project-count[data-over="true"]{opacity:1;color:#c2410c} .bt-project-row-actions{display:flex;gap:8px;align-items:center} .bt-project-members{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px} .bt-project-member{display:flex;gap:10px;align-items:baseline;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08))} @@ -124,6 +128,14 @@ export const SOURCE_LABELS = { bot: 'Bots', group: 'Groups', workflow: 'Workflow const text = value => (typeof value === 'string' ? value.trim() : '') +// #1887: the memory box warns about the bound before the click, the way the host bounds it. One +// entry is at most `MAX_ENTRY` (`src/host/constants.js`, #1847) and the host counts its code points +// (#1886), so the counter counts the same unit on the trimmed text the host would store — which is +// also why the field carries no `maxLength`: that counts UTF-16 units and would call a 600-emoji +// entry twice its real length. +export const MEMORY_TEXT_MAX = 600 +const memoryLength = (value) => [...text(value)].length + // #1855: the column a finished card reads. The host sends those cards apart (`done`) and the view // keeps them out of every group of work in flight. const DONE_COLUMN = 'done' @@ -753,6 +765,11 @@ export function ProjectSettingsPanel({ project, assistantMine, call: request, on const [kind, setKind] = useState('bot') const [memberId, setMemberId] = useState('') const [note, setNote] = useState('') + // #1887: an empty box has nothing to save and an over-long one can never land, so neither + // reaches the host — the reader sees the state before the click instead of a refusal after it. + const noteLength = memoryLength(note) + const noteOver = noteLength > MEMORY_TEXT_MAX + const noteSaveDisabled = text(note) === '' || noteOver // #1845: the digest opt-out is per owner, and the host keeps it on `owners[]`; the // Project record has no such field, so reading it there showed a paused digest as unticked and // the next Save turned it back on. `project-read` answers the caller's own record beside the @@ -794,8 +811,20 @@ export function ProjectSettingsPanel({ project, assistantMine, call: request, on // never as a third kind the host would refuse (#1820). h('label', { htmlFor: 'bt-project-note' }, t('Add to the assistant memory')), h('textarea', { id: 'bt-project-note', value: note, onChange: event => setNote(event.target.value) }), + h('div', { className: 'bt-project-count', 'data-role': 'project-memory-count', 'data-over': noteOver ? 'true' : 'false' }, `${noteLength}/${MEMORY_TEXT_MAX}`), h('div', { className: 'bt-project-row-actions' }, - h('button', { type: 'button', className: 'bt-soft bt-soft-sm', 'data-action': 'project-memory-save', onClick: () => run('project-memory-write', { id, kind: 'decision', text: text(note) }, t('Memory saved.')) }, t('Save memory')))), + h('button', { + type: 'button', + className: 'bt-soft bt-soft-sm', + 'data-action': 'project-memory-save', + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. + onClick: () => { + if (noteSaveDisabled) return + run('project-memory-write', { id, kind: 'decision', text: text(note) }, t('Memory saved.')) + }, + }, t('Save memory')))), h('div', { className: 'bt-project-status-line', role: 'status' }, status), h('div', { className: 'bt-project-row-actions' }, h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: () => onClose?.() }, t('Back to board')))) diff --git a/patches/ds-bot/files/test/project-board-panels.test.js b/patches/ds-bot/files/test/project-board-panels.test.js index 0ed3f0d5c..0de1b4e01 100644 --- a/patches/ds-bot/files/test/project-board-panels.test.js +++ b/patches/ds-bot/files/test/project-board-panels.test.js @@ -304,6 +304,9 @@ test('a memory kind the host does not know writes nothing, and the panel quotes try { const refused = async () => { throw Object.assign(new Error('Only the owner of this Project may change it'), { code: 'bot/forbidden' }) } const draw = () => render(ProjectSettingsPanel, { project: project(), call: refused, onClose() {} }) + // #1887: an empty box no longer reaches the host, so the entry is one the panel allows; the + // refusal below is the owner gate, which is what this half of the test is about. + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: 'ship on Friday' } }) let tree = draw() byAction(tree, 'project-memory-save').props.onClick() assert.equal(await statusAfter(draw, '只有这个项目的主人可以修改'), '只有这个项目的主人可以修改') @@ -345,36 +348,29 @@ test('#1847: the memory write door refuses empty and over-long text and stores 6 assert.equal(memory.decisions[0].text, exact, 'the text is what was sent') }) -// The other half of #1847: the panel quotes the host's new refusals in Chinese, so the reader sees -// why Save did nothing instead of a success line, and no row is added. -test('#1847: the panel shows the Chinese refusal for an empty or over-long memory box', async (t) => { +// The other half of #1847: the panel quotes the host's refusals in Chinese. Since #1887 the panel +// refuses an empty or over-long box before the click, so the one refusal still reachable is the two +// bounds disagreeing — an emoji entry the client counts as exactly 600 code points while a host +// that still counted UTF-16 units (#1886) answers "at most 600 characters". The reader must see +// why Save did nothing, never a success line, and no row may land. +test('#1847: the panel shows the Chinese refusal when the host refuses the memory text', async (t) => { + resetSlots() const rt = await startHost(t) const call = (endpoint, payload) => rt.projectEndpoint(endpoint, payload) const off = installLocale(fakeLocale('zh')) try { - // The hook slots outlive a render, so the box starts where the previous test left it; empty it - // explicitly before pressing Save. - let tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - all(tree).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: '' } }) - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - byAction(tree, 'project-memory-save').props.onClick() - await settle() - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - let status = all(tree).find(node => node.props?.role === 'status') - assert.equal(text(status), '项目记忆条目需要文字') - assert.ok(!text(status).includes('Memory saved.'), 'a refusal is never painted as a success') - - const note = all(tree).find(node => node.props?.id === 'bt-project-note') - note.props.onChange({ target: { value: 'x'.repeat(601) } }) - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - byAction(tree, 'project-memory-save').props.onClick() - await settle() - tree = render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) - status = all(tree).find(node => node.props?.role === 'status') - assert.equal(text(status), '项目记忆条目最多 600 个字符') + const draw = () => render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: '🙂'.repeat(600) } }) + const refused = Object.assign(new Error('A Project memory entry is at most 600 characters'), { code: 'bot/invalid' }) + const racing = () => render(ProjectSettingsPanel, { project: project(), call: async () => { throw refused }, onClose() {} }) + assert.ok(!byAction(racing(), 'project-memory-save').props.disabled, 'the client counts the entry at the bound and lets the click through') + byAction(racing(), 'project-memory-save').props.onClick() + assert.equal(await statusAfter(racing, '项目记忆条目最多 600 个字符'), '项目记忆条目最多 600 个字符') + assert.ok(!drawnStatus(racing).includes('记忆已保存。'), 'a refusal is never painted as a success') + assert.equal(hostText('A Project memory entry needs text'), '项目记忆条目需要文字', 'the empty-text refusal is translated too, for a host that refuses it') const memory = (await call('project-memory', { id: 'project-1' })).memory - assert.deepEqual(memory.decisions, [], 'neither refusal adds a row') + assert.deepEqual(memory.decisions, [], 'the refusal adds no row') } finally { off() } @@ -385,6 +381,8 @@ test("the settings panel shows the host's refusal instead of pretending the save const refused = Object.assign(new Error('Only the owner of this Project may change it'), { code: 'bot/forbidden' }) const save = async () => { throw refused } const draw = () => render(ProjectSettingsPanel, { project: { id: 'project-1' }, call: save, onClose() {} }) + // #1887: the box needs an entry the panel allows, or Save is disabled and no host refusal is asked. + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value: 'ship on Friday' } }) const tree = draw() byAction(tree, 'project-memory-save').props.onClick() await waitFor(() => (/Only the owner of this Project/.test(drawnStatus(draw)) ? true : undefined)) @@ -536,3 +534,53 @@ test('the owner\'s chat entry opens the Project assistant, and only the owner\'s assert.equal(projectAgentFor({ ownerId: 'owner-1' }, 'owner-1'), null, 'a Bot outside a Project offers no entry') assert.equal(projectAgentFor({ projectId: 'project-1', ownerId: 'owner-1', remote: {} }, 'owner-1'), null, "a friend's Bot offers no entry") }) + +// #1887: the memory box warns before the click. The field had no counter and Save stayed enabled +// with nothing typed, so the first news of the 600 bound was the host's refusal after the click +// (#1847 already refuses at the door — the defect is the silence before it, not a blank entry +// landing). The counter counts code points, the unit the host bounds (#1886), so an emoji entry at +// the bound is not refused for what `maxLength` would have counted as twice as many UTF-16 units. +const memoryCount = (tree) => all(tree).find(node => node.props?.['data-role'] === 'project-memory-count') +const memorySave = (tree) => byAction(tree, 'project-memory-save') + +test('#1887: the memory box counts code points and disables Save before the click', async (t) => { + resetSlots() + const rt = await startHost(t) + let calls = 0 + const call = (endpoint, payload) => { calls += 1; return rt.projectEndpoint(endpoint, payload) } + const draw = () => render(ProjectSettingsPanel, { project: project(), call, onClose() {} }) + const type = (value) => { + all(draw()).find(node => node.props?.id === 'bt-project-note').props.onChange({ target: { value } }) + return draw() + } + + // Empty: there is nothing to save, and the reader is told before pressing anything. + let tree = type('') + assert.equal(memorySave(tree).props.disabled, true, 'an empty memory box disables Save') + assert.equal(text(memoryCount(tree)), '0/600', 'the counter reads the empty box') + memorySave(tree).props.onClick() + await settle() + + // Whitespace is not an entry either: the host refuses it as no text, so the button agrees. + tree = type(' \n ') + assert.equal(memorySave(tree).props.disabled, true, 'a whitespace-only memory box disables Save') + + // One past the bound: the counter says so and Save is disabled before the click. + const emoji = '🙂' + tree = type(emoji.repeat(601)) + assert.equal(text(memoryCount(tree)), '601/600', 'the counter counts code points, not UTF-16 units') + assert.equal(memoryCount(tree).props['data-over'], 'true', 'the counter is in its over-limit state') + assert.equal(memorySave(tree).props.disabled, true, 'an over-long entry disables Save') + memorySave(tree).props.onClick() + await settle() + + // The bound itself is an entry: Save is enabled and the counter reads it whole. + tree = type(emoji.repeat(600)) + assert.equal(text(memoryCount(tree)), '600/600', 'an entry at the bound reads the bound') + assert.equal(memoryCount(tree).props['data-over'], 'false', 'and the counter is not in its over-limit state') + assert.ok(!memorySave(tree).props.disabled, 'the bound itself is allowed') + + assert.equal(calls, 0, 'neither a disabled nor an over-limit Save calls the endpoint') + const memory = (await rt.projectEndpoint('project-memory', { id: 'project-1' })).memory + assert.deepEqual(memory.decisions, [], 'and neither state writes anything') +})