From 808876f9ee2dcb492c65704e5527918bb813a8e9 Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:38:56 +0800 Subject: [PATCH] client: a locked window's overview keeps the model, permission and folder values P-547: the overview dropped the Model, Permissions and Working folder cards whole in a read-only window, so their saved values went with the write controls, though the name above is already value-kept/control-removed. Each card now keeps its title and its value as plain text (ReadOnlyOverview) and draws no menu, preset or button; the writable branch is untouched, and the bundle plus patches 0008/0012 are regenerated from it. --- core/client.js | 49 +- core/src/client/details-drawer.js | 37 +- core/test/details-overview-readonly.test.js | 49 + docs/subsystems/core-client.md | 2 +- patches/ds-bot/0008-client-bundle.patch | 8638 +++++++++-------- patches/ds-bot/0012-room-membership.patch | 47 +- .../test/details-overview-readonly.test.js | 49 + 7 files changed, 4915 insertions(+), 3956 deletions(-) diff --git a/core/client.js b/core/client.js index 93e045f7a..f08ba35f9 100644 --- a/core/client.js +++ b/core/client.js @@ -19793,6 +19793,49 @@ function PermissionRow({ bot, roster, actions, run }) { ) ); } +function ReadOnlyOverview({ bot, roster }) { + const shown = modelOf(bot, roster); + const models = roster.models ?? []; + const catalog = roster.permissions; + const preset = catalog?.options?.find((option) => option.value === (bot.permission ?? catalog?.defaultPreset)); + const folder = bot.workDir ?? ""; + return (0, import_react44.createElement)( + import_react44.Fragment, + null, + (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)( + "div", + { className: "bt-model-copy bt-readonly-value", title: shown?.ref }, + (0, import_react44.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Not set")), + shown ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? shown.provider : t("{provider} · not available", { provider: shown.provider })) : null + ) + ), + preset ? (0, import_react44.createElement)( + "div", + { key: "permission", className: "bt-drawer-model" }, + (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Permissions")), + (0, import_react44.createElement)( + "div", + { className: "bt-model-copy bt-readonly-value" }, + (0, import_react44.createElement)("span", { className: "bt-model-name" }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name) + ) + ) : null, + (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)( + "div", + { className: "bt-model-copy bt-readonly-value", title: folder || void 0 }, + (0, import_react44.createElement)("span", { className: "bt-model-name" }, folder ? folderName(folder) : t("Team workspace")), + folder ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, folder) : null + ) + ) + ); +} function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -20186,9 +20229,11 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a ] : null, bot && !bot.remote && tab === "details" ? [ // P-542/P-487: the overview's own model, permission and folder entries are writes too, and a - // locked window refuses every one of them. Say so once at the top and draw the read half only; - // the name above is already text, and the settings gear still opens the page that explains it. + // locked window refuses every one of them. Say so once at the top and draw the read half only, + // which is the values themselves (P-547) — the name above is already text, and the settings + // gear still opens the page that explains the window. lockedWindow(roster) ? (0, import_react44.createElement)("div", { key: "read-only", className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + lockedWindow(roster) ? (0, import_react44.createElement)(ReadOnlyOverview, { key: "read", bot, roster }) : null, lockedWindow(roster) ? null : (0, import_react44.createElement)( "div", { key: "model", className: "bt-drawer-model" }, diff --git a/core/src/client/details-drawer.js b/core/src/client/details-drawer.js index 48d466879..bb88487e5 100644 --- a/core/src/client/details-drawer.js +++ b/core/src/client/details-drawer.js @@ -8,7 +8,7 @@ import { useEscape } from './overlay-hooks.js' import { BotSettingsForm } from './bot-settings.js' import { FaceLead, FacePersonRow, GroupSettingsForm, ROOM_MODES, facePeople, roomSubtitle, shownMode } from './group-settings.js' import { ModelMenu, modelOf } from './model-menu.js' -import { FolderMenu } from './folder-menu.js' +import { FolderMenu, folderName } from './folder-menu.js' import { MemoryRow } from './memory-panel.js' import { RoomSchedules, ScheduleSummary, lockedWindow } from './schedules.js' import { Segmented, useGlide, useMorph } from './motion.js' @@ -53,6 +53,35 @@ function PermissionRow({ bot, roster, actions, run }) { }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name)))) } +// P-547: a locked window refuses the overview's writes, not its reading. The model, permission and +// folder cards kept their writer's form or vanished whole, so their saved values went with the +// controls; here each keeps its title and its value as plain text — the shape the name above +// already has (#2391: the value stays, the control goes) — and no menu, preset or button is drawn. +function ReadOnlyOverview({ bot, roster }) { + const shown = modelOf(bot, roster) + const models = roster.models ?? [] + const catalog = roster.permissions + const preset = catalog?.options?.find(option => option.value === (bot.permission ?? catalog?.defaultPreset)) + const folder = bot.workDir ?? '' + return h(Fragment, null, + h('div', { key: 'model', className: 'bt-drawer-model' }, + h('div', { className: 'bt-section-title' }, t('Model')), + h('div', { className: 'bt-model-copy bt-readonly-value', title: shown?.ref }, + h('span', { className: 'bt-model-name' }, shown?.name ?? t('Not set')), + shown ? h('span', { className: 'bt-model-provider' }, shown.served || models.length === 0 + ? shown.provider + : t('{provider} · not available', { provider: shown.provider })) : null)), + preset ? h('div', { key: 'permission', className: 'bt-drawer-model' }, + h('div', { className: 'bt-section-title' }, t('Permissions')), + h('div', { className: 'bt-model-copy bt-readonly-value' }, + h('span', { className: 'bt-model-name' }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name))) : null, + h('div', { key: 'folder', className: 'bt-drawer-model' }, + h('div', { className: 'bt-section-title' }, t('Working folder')), + h('div', { className: 'bt-model-copy bt-readonly-value', title: folder || undefined }, + h('span', { className: 'bt-model-name' }, folder ? folderName(folder) : t('Team workspace')), + folder ? h('span', { className: 'bt-model-provider' }, folder) : null))) +} + function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs(map => map[bot.id] ?? EMPTY_TABS) const pages = useBrowserPages(map => map) @@ -379,9 +408,11 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro ] : null, bot && !bot.remote && tab === 'details' ? [ // P-542/P-487: the overview's own model, permission and folder entries are writes too, and a - // locked window refuses every one of them. Say so once at the top and draw the read half only; - // the name above is already text, and the settings gear still opens the page that explains it. + // locked window refuses every one of them. Say so once at the top and draw the read half only, + // which is the values themselves (P-547) — the name above is already text, and the settings + // gear still opens the page that explains the window. lockedWindow(roster) ? h('div', { key: 'read-only', className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null, + lockedWindow(roster) ? h(ReadOnlyOverview, { key: 'read', bot, roster }) : null, lockedWindow(roster) ? null : h('div', { key: 'model', className: 'bt-drawer-model' }, h('div', { className: 'bt-section-title' }, t('Model')), h(ModelMenu, { bot, roster, actions, run, wide: true }), diff --git a/core/test/details-overview-readonly.test.js b/core/test/details-overview-readonly.test.js index 422f3eb6d..70ebd07dd 100644 --- a/core/test/details-overview-readonly.test.js +++ b/core/test/details-overview-readonly.test.js @@ -200,3 +200,52 @@ test('P-544: a read-only group overview says why once and draws the name as plai assert.deepEqual(modelTriggers(entries), [], 'a group never had a model menu') assert.deepEqual(permissionRows(entries), [], 'a group never had permission presets') }) + +// P-547: the same locked window dropped the overview's Model, Permissions and Working folder +// cards whole, so their values went with the controls even though the name above is already +// "value kept, control removed" (`details-drawer.js`'.bt-drawer-name' as plain text). Product's +// probe on a locked window read `model name drawn = false`, `permission label = false`, +// `workDir = false`; in a writable window all three were true. A locked window refuses the +// writes, not the reading, so the three values stay on the page as text and none of the three +// controls is drawn. +const withModel = { ...bot, model: 'deepseek/deepseek-v4.1-flash', workDir: '/home/xuke/flybot' } +const modelsCatalog = [{ ref: 'deepseek/deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash', providerName: 'DeepSeek' }] +const rosterModelWith = (readOnly) => ({ + ...base, bots: [withModel], byId: { a: withModel }, models: modelsCatalog, + ...(readOnly === undefined ? {} : { readOnly }), +}) +const readOnlyValues = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-readonly-value')) +// The value line itself, without the note under it (the provider, the full path). +const readOnlyNames = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-model-name')).map(({ node }) => ownText(node).trim()) +const sectionTitles = (entries) => nodes(entries).filter(node => hasClass(node, 'bt-section-title')).map(node => ownText(node).trim()) + +test('P-547: a locked overview keeps the model, permission and folder values and draws no control', () => { + slots = [] + const entries = tree(rosterModelWith(SENTENCE)) + assert.equal(statusLine(entries).length, 1, "the host's sentence is still drawn once") + assert.equal(readOnlyValues(entries).length, 3, 'the three values are drawn, not dropped with their controls') + const names = readOnlyNames(entries) + assert.equal(names[0], 'DeepSeek V4.1 Flash', 'the model name is on the page') + assert.equal(names[1], 'Workspace', 'the permission preset is on the page, by its label') + assert.equal(names[2], 'flybot', 'the working folder is on the page by its last segment') + const titles = sectionTitles(entries) + for (const title of ['Model', 'Permissions', 'Working folder']) assert.ok(titles.includes(title), `${title} keeps its section`) + const folder = readOnlyValues(entries)[2].node + assert.equal(folder.props.title, '/home/xuke/flybot', 'the full folder path is kept as the value title') + assert.deepEqual(modelTriggers(entries), [], 'no model menu the host would refuse') + assert.deepEqual(permissionRows(entries), [], 'no permission preset the host would refuse') + assert.deepEqual(folderTriggers(entries), [], 'no working-folder menu the host would refuse') + assert.deepEqual(nodes(entries).filter(node => node.props?.['aria-haspopup'] === 'menu'), [], + 'no anchored menu of any kind is drawn in a locked overview') +}) + +test('P-547: a writable overview keeps its controls and draws no read-only value', () => { + slots = [] + const entries = tree(rosterModelWith(undefined)) + assert.deepEqual(readOnlyValues(entries), [], 'the read-only values are a locked window only') + assert.equal(modelTriggers(entries).length, 1, 'the model menu is still the writable control') + assert.equal(permissionRows(entries).length, 1, 'the permission presets are still the writable control') + assert.equal(folderTriggers(entries).length, 1, 'the working-folder menu is still the writable control') + assert.equal(ownText(modelTriggers(entries)[0].node).includes('DeepSeek V4.1 Flash'), true, + 'the writable trigger still shows the same model name') +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 73fb01482..509dc7857 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -130,7 +130,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second - The "@" reader is shared with the host (`core/src/shared/mentions.js`, bundled into `core/client.js`). #287 added `unmatchedMentions` there (the `@` words that name nobody, used by the host for the face-room hint); `mentionSpans` and what the bubbles draw are unchanged. - A friend's Bot settings surface is not the local editor (#160): no rename, avatar editor, model, folder, instructions, Main Bot, Hide, Duplicate, or Delete. The details gear and context menu say **Bot settings**. The menu is Mark as Unread, Bot settings, and Copy conversation ID. Per-friend **Auto-send** and **Remove this Bot** stay on that page (`flybot.bot.details`); Remove closes the overlay so a stale local editor cannot appear. The Bots settings list is local Bots only. - A friend's Bot details overview is that compact panel: handle plus `FlyBotDetails` (Auto-send and a muted Remove link), no Browser tab, no Usage / model / folder. Opening the gear uses `remote-bot-settings` (`RemoteBotSettingsSheet`): a bottom sheet on narrow viewports and a centered card from 520px up, with the scrim behind the dialog (`z-index` stacking like `bt-settings` / `bt-mem`) so the sheet stays clickable through enter/leave animations. The surrounding DSH settings-shell chrome (back/title) is not redesigned (#166). The sheet takes focus when it opens (`tabIndex -1`, `preventScroll`), closes on Escape from its own `onKeyDown` as well as `useEscape`, and gives focus back without scrolling; in the tree a friend's `.bt-row-wrap` (row plus gear) shrinks like a plain row (`.bt-tree-item>.bt-row-wrap`), so the gear never overflows the sidebar and focusing it cannot scroll it sideways (#274). On every wide row (own Bots too) the gear's focus and click run `keepSidebarStill`, which puts back any sideways scroll a focus left on the row's ancestors or the page, now and after the settings dialog opened (#421). The Settings nav's last entry reads **Harness settings ↗** with a tooltip saying it opens DeepSeek Harness's own settings in a separate window; FlyBot does not embed or retitle that dialog (#421). -- An own Bot's settings (`BotSettingsForm`) draw the `flybot.bot.details` entries once, as a titled **Sharing** card right after **Instructions** (`FlyBotDetails` with `wrap` → `Card`), so sharing is not at the end of the page; with no entry there is no card. Bot settings and group settings both end with a **More** card (Duplicate / Delete). A write the host refuses is reported on the dialog's own line under its heading, outside the page's scrolling box: `SettingsDialog` owns the sentence `run` stores, and `.bt-settings-scroll` no longer holds it, so scrolling down to the control that was pressed cannot leave the refusal above the viewport with the switch apparently unmoved ([#2194](https://github.com/ThinkFlowLab/FlyBot/issues/2194)). A window that holds the team read-only says so in that same header: `SettingsDialog` prints the host's own sentence from `roster.readOnly` as a `role="status"` line under the heading and above the refused-write line, so the standing state comes first and the answer to one pressed control sits closest to it, and a blank or missing `readOnly` draws nothing at all (#2199). The details drawer's own Settings view is not that dialog and had no header sentence: it now reads the same window lock (`lockedWindow`, `schedules.js`) and, in a read-only window, draws the host's sentence in the form's place instead of mounting `BotSettingsForm` or `GroupSettingsForm`, so no write control the host would refuse is drawn for a Bot or a group, and a writable window keeps the form unchanged (#2387). A refused write in that view follows the same rule as the dialog (#2385): the `bt-error` line is a sibling above `bt-drawer-body bt-set-body`, not its first child, so a press at the bottom of the group page — Pin, Hide, Make admin, Remove — writes the host's refusal where the press happened instead of at the top of the box it was scrolled past. The drawer's overview view holds the same two rules: its refusal line is a sibling above `bt-drawer-body`, and in a read-only window the Bot's name is plain text with no model, permission or working-folder entry beside it, above a `role="status"` line carrying the host's own sentence, so the overview offers only what the host will not refuse. A group's overview follows it (#2391 left it to P-544): the name is plain text with no rename control, `Remove my Bot` is gone, and the host's one sentence sits above the reads — a shared room's approval card keeps its headline and rows and drops its decisions — while a writable window keeps every control. Tests: `core/test/details-bot-settings-readonly.test.js`, `core/test/details-drawer-refusal-line.test.js`, `core/test/details-overview-readonly.test.js`, `core/test/details-group-overview-readonly.test.js`. The composer card's `:focus-within` border is 55% ink, hover stays 30% (#406). Keyboard Tab order, checked in headless Chrome on a mock bench (#406): sidebar (Search, New…, the Bots, Connect apps, Account) → chat header pill → message box → Add, Dictate, voice → the turn stats; the `shell.overlay` layer (approval cards) comes after the main column in the DOM, and no stop focuses an invisible element. The message box comes before **+** because DSH's DOM puts the input first and FlyBot's CSS only reorders it visually. +- An own Bot's settings (`BotSettingsForm`) draw the `flybot.bot.details` entries once, as a titled **Sharing** card right after **Instructions** (`FlyBotDetails` with `wrap` → `Card`), so sharing is not at the end of the page; with no entry there is no card. Bot settings and group settings both end with a **More** card (Duplicate / Delete). A write the host refuses is reported on the dialog's own line under its heading, outside the page's scrolling box: `SettingsDialog` owns the sentence `run` stores, and `.bt-settings-scroll` no longer holds it, so scrolling down to the control that was pressed cannot leave the refusal above the viewport with the switch apparently unmoved ([#2194](https://github.com/ThinkFlowLab/FlyBot/issues/2194)). A window that holds the team read-only says so in that same header: `SettingsDialog` prints the host's own sentence from `roster.readOnly` as a `role="status"` line under the heading and above the refused-write line, so the standing state comes first and the answer to one pressed control sits closest to it, and a blank or missing `readOnly` draws nothing at all (#2199). The details drawer's own Settings view is not that dialog and had no header sentence: it now reads the same window lock (`lockedWindow`, `schedules.js`) and, in a read-only window, draws the host's sentence in the form's place instead of mounting `BotSettingsForm` or `GroupSettingsForm`, so no write control the host would refuse is drawn for a Bot or a group, and a writable window keeps the form unchanged (#2387). A refused write in that view follows the same rule as the dialog (#2385): the `bt-error` line is a sibling above `bt-drawer-body bt-set-body`, not its first child, so a press at the bottom of the group page — Pin, Hide, Make admin, Remove — writes the host's refusal where the press happened instead of at the top of the box it was scrolled past. The drawer's overview view holds the same two rules: its refusal line is a sibling above `bt-drawer-body`, and in a read-only window the Bot's name is plain text with no model, permission or working-folder control beside it, above a `role="status"` line carrying the host's own sentence, so the overview offers only what the host will not refuse. Since P-547 those three cards keep their titles and their saved values as plain text as well (`ReadOnlyOverview`: the model's name and provider, the permission preset's label, the working folder's last segment with the full path as its `title`), because the window refuses the writes, not the reading — a value that vanishes with its control leaves the page saying less than the host knows; a writable window keeps every control and draws none of those values twice. Tests: `core/test/details-overview-readonly.test.js`. A group's overview follows it (#2391 left it to P-544): the name is plain text with no rename control, `Remove my Bot` is gone, and the host's one sentence sits above the reads — a shared room's approval card keeps its headline and rows and drops its decisions — while a writable window keeps every control. Tests: `core/test/details-bot-settings-readonly.test.js`, `core/test/details-drawer-refusal-line.test.js`, `core/test/details-overview-readonly.test.js`, `core/test/details-group-overview-readonly.test.js`. The composer card's `:focus-within` border is 55% ink, hover stays 30% (#406). Keyboard Tab order, checked in headless Chrome on a mock bench (#406): sidebar (Search, New…, the Bots, Connect apps, Account) → chat header pill → message box → Add, Dictate, voice → the turn stats; the `shell.overlay` layer (approval cards) comes after the main column in the DOM, and no stop focuses an invisible element. The message box comes before **+** because DSH's DOM puts the input first and FlyBot's CSS only reorders it visually. - Every listed Bot row in wide sidebar mode is a `bt-row-wrap`: the session `bt-row` and a sibling gear (`bt-row-settings`, hover/focus) that opens Edit Bot for an own Bot or Bot settings for a friend's (#166, #276). - The Session header is the shell's own: FlyBot registers the Bot pill in `conversation.session.header.utilities` rather than claiming the single `conversation.session.header` slot, so the seats that header renders — the lineage title and `conversation.session.header.actions`, where `@deepseek-ai/ui-subagent` puts the subagent catalog — keep rendering (#803). The floating header style FlyBot used to draw over the Session header is gone. - What the pill does to the page is not in the pill and not in a seat of its own (#803). The shell draws `.utilities` — and the pill with it — only while its chrome shows, which a Session without a first message hides, so a side effect parked in the pill stops there: the friend's empty-chat hint, `body[data-bt-friend-chat]`, the `Message {name}` composer label, the visit ledger, `body[data-bt-room]` and the face-room `@` filter. `installHeaderEffects` (`src/client/header-effects.js`) is installed once from `apply` and keeps them on the Session in the main view. It carries no React, and each effect has its own dependency key: the sessions list answers before the roster, so a single "did the Session change" guard would drop a friend's flags on the first pass, while `beginVisit`/`endVisit` keyed on more than the Session would open a second visit for a chat that was only just opened. `test/header-effects.test.js` drives those transitions on jsdom; `test/header-slot.test.js` pins that no such effect runs in `HeaderPill` and that FlyBot claims no other header seat. diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index c08220570..77f909124 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -6336,7 +6336,7 @@ diff --git a/client.js b/client.js @keyframes bt-typing-in{from{opacity:0;transform:scale(.9)}} @keyframes bt-typing-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.92);filter:blur(3px)}} @keyframes bt-typing-label{from{opacity:0;filter:blur(5px);transform:translateY(3px)}} -@@ -2676,2104 +2676,4377 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} +@@ -2676,4494 +2676,7999 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} [data-bt-enter="new"]{animation:none} [data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out} } @@ -6447,6 +6447,13 @@ diff --git a/client.js b/client.js -.bt-earlier .bt-event{margin:4px 0} -.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} -.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} +-`; +-function installStyles(text, tag) { +- const style = document.createElement("style"); +- style.dataset.dshBot = tag; +- style.textContent = text; +- document.head.appendChild(style); +- return style; +@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} +@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} +[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} @@ -7156,7 +7163,16 @@ diff --git a/client.js b/client.js + ) + ) : null + ); -+} + } +-var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; +-var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; +-function sheetClassNames(sheet) { +- const names = []; +- const visit = (rules) => { +- for (const rule of rules) { +- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); +- if (rule.cssRules) visit(rule.cssRules); +- } +function AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react9.useState)(""); + const [purpose, setPurpose] = (0, import_react9.useState)(""); @@ -7272,13 +7288,7 @@ diff --git a/client.js b/client.js +.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}} - `; --function installStyles(text, tag) { -- const style = document.createElement("style"); -- style.dataset.dshBot = tag; -- style.textContent = text; -- document.head.appendChild(style); -- return style; ++`; + +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { @@ -7293,13 +7303,25 @@ diff --git a/client.js b/client.js + bot: local ? local.name : remoteBotName(p, "Bot"), + mine: Boolean(local) + }; -+ }; + }; +- try { +- visit(sheet.cssRules); +- } catch { + const seats = records.filter((p) => p.leftSeq === void 0).map(seat); + for (const id of room2.members ?? []) { + const bot = roster.byId[id]; + if (!bot || seats.some((item) => item.local?.id === id)) continue; + seats.push({ key: id, local: bot, record: { id }, person: cleanPeerName(roster.self?.displayName, ""), bot: bot.name, mine: true }); -+ } + } +- return names; +-} +-function compileFragmentSelectors(text, names) { +- const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { +- if (!compiled.has(fragment)) { +- const exact = []; +- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); + if (!left) return seats; + const gone = records.filter((p) => p.leftSeq !== void 0 && !seats.some((item) => item.key === (p.handle || p.id))).map((item) => ({ ...seat(item), left: true })); + return [...seats, ...gone]; @@ -7330,19 +7352,26 @@ diff --git a/client.js b/client.js + if (known) { + known.seats.push(seat); + continue; -+ } + } +- return compiled.get(fragment); +- }); + 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)]; } --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 installCompiledStyles(text, tag) { +- const style = installStyles("", tag); +- const cache = /* @__PURE__ */ new WeakMap(); +- const render = () => { +- const names = /* @__PURE__ */ new Set(); +- for (const sheet of document.styleSheets) { +- if (sheet.ownerNode?.dataset?.dshBot) continue; +- let list = cache.get(sheet); +- if (!list) { +- list = sheetClassNames(sheet); +- cache.set(sheet, list); +- } +- for (const name of list) names.add(name); +function facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; +} @@ -7395,10 +7424,27 @@ diff --git a/client.js b/client.js + } finally { + setBusy(false); } +- const next = compileFragmentSelectors(text, names); +- if (style.textContent !== next) style.textContent = next; +- }; +- render(); +- let timer; +- const schedule = () => { +- clearTimeout(timer); +- timer = setTimeout(render, 120); +- }; +- const onLoad = (event) => { +- if (event.target instanceof HTMLLinkElement) schedule(); +- }; +- const observer = new MutationObserver(schedule); +- observer.observe(document.head, { childList: true }); +- document.addEventListener("load", onLoad, true); +- return () => { +- observer.disconnect(); +- document.removeEventListener("load", onLoad, true); +- clearTimeout(timer); +- style.remove(); }; -- try { -- visit(sheet.cssRules); -- } catch { + if (!waiting) { + const failed = held.status === "failed"; + return (0, import_react10.createElement)( @@ -7410,8 +7456,7 @@ diff --git a/client.js b/client.js + failed && canRetry(held) ? locked ? readOnlyLine() : (0, import_react10.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null, + error ? (0, import_react10.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null + ); - } -- return names; ++ } + const at = typeof held.createdAt === "number" ? held.createdAt : Date.parse(held.createdAt); + const age = Number.isFinite(at) && at > 0 ? ` · ${relativeShort(at)}` : null; + const reason = reasonLine(held.reason); @@ -7435,36 +7480,74 @@ diff --git a/client.js b/client.js + custom ? (0, import_react10.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react10.createElement)(custom.component, { held, decide, call: call3, actions, readOnly: locked === true })) : plain + ); } --function compileFragmentSelectors(text, names) { -- const compiled = /* @__PURE__ */ new Map(); -- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { -- if (!compiled.has(fragment)) { -- const exact = []; -- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); -- } -- return compiled.get(fragment); -- }); -+ + +-// 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]); +// 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 installCompiledStyles(text, tag) { -- const style = installStyles("", tag); -- const cache = /* @__PURE__ */ new WeakMap(); -- const render = () => { -- const names = /* @__PURE__ */ new Set(); -- for (const sheet of document.styleSheets) { -- if (sheet.ownerNode?.dataset?.dshBot) continue; -- let list = cache.get(sheet); -- if (!list) { -- list = sheetClassNames(sheet); -- cache.set(sheet, list); -- } -- for (const name of list) names.add(name); +-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 +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(); @@ -7487,30 +7570,19 @@ diff --git a/client.js b/client.js + if (tabId !== void 0 && tabId !== null && typeof actions.browserNavigate === "function") { + void actions.browserNavigate(tabId, href); + return true; - } -- const next = compileFragmentSelectors(text, names); -- if (style.textContent !== next) style.textContent = next; -- }; -- render(); -- let timer; -- const schedule = () => { -- clearTimeout(timer); -- timer = setTimeout(render, 120); -- }; -- const onLoad = (event) => { -- if (event.target instanceof HTMLLinkElement) schedule(); ++ } + if (typeof window !== "undefined" && typeof window.open === "function") window.open(href, "_blank", "noopener,noreferrer"); + return true; }; -- 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(); -+} +- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); +- const box = shapeOf(look.shape).size; +- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); + } +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) +function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, msgKeys, useUi, lead, mentions }) { + const openLink = bubbleLinkClick({ actions, subject: selfId ?? sessionId }); + const onClick = (event) => { @@ -7520,7 +7592,7 @@ diff --git a/client.js b/client.js + event.preventDefault(); + event.stopPropagation(); + actions.openSession(target); - }; ++ }; + const reactions = useUi ? useUi((state) => msgKey || msgKeys ? msgKeys ? state.reactions ?? null : state.reactions?.[msgKey] ?? null : null) : null; + const reactKey = (index) => msgKeys ? msgKeys[index] : msgKey; + const reactIndex = (index) => msgKeys ? 0 : index; @@ -7557,31 +7629,10 @@ diff --git a/client.js b/client.js + }, emoji))) : null + ); + }) -+ ); + ); } -- --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); +-function AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; +var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text2) { @@ -7595,63 +7646,43 @@ diff --git a/client.js b/client.js + if (name && (who === name || who.endsWith(` (${name})`))) return true; + } + return false; -+} + } +-function MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); +function useDismiss(open, close, refs) { + (0, import_react11.useEffect)(() => { + if (!open) return void 0; + const onDown = (event) => { + if (refs.some((ref) => ref.current?.contains(event.target))) return; + close(); - }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); ++ }; + const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); + close(); + } - }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); ++ }; + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); - return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); ++ return () => { + window.removeEventListener("mousedown", onDown); + window.removeEventListener("keydown", onKey, true); - }; -- }, [enabled]); ++ }; + }, [open]); } --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 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 MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react11.useState)(null); + const [place, setPlace] = (0, import_react11.useState)(null); @@ -7664,15 +7695,31 @@ diff --git a/client.js b/client.js + if (panel === kind) { + close(); + return; -+ } + } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; +-} +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null + 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); - }; -- 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 copy2 = () => { + void navigator.clipboard?.writeText(text2).then(() => { + setCopied(true); @@ -7709,8 +7756,31 @@ diff --git a/client.js b/client.js + item(t("Copy"), CopyIcon, copy2), + item(t("Download"), DownloadIcon, download2) + ), document.body) : null -+ ); -+} + ); + } +-function clusterSeats(count, frame) { +- if (count === 1) return [[0, 0, frame]]; +- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; +- if (count === 3) { +- const size = frame * 5 / 9; +- const rest = frame - size; +- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; +- } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; +-} +-function RoomAvatar({ room: room2, roster, size = 36 }) { +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const people = members.length === 1 ? [members[0], null] : members; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -7722,7 +7792,42 @@ diff --git a/client.js b/client.js + for (const part of graphemes(line2)) { + if (kept.length + part.length > PEEK_MAX - CUT.length) break; + kept += part; -+ } + } +- 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)); +- }) +- ); +-} +- +-// src/client/icons.js +-var import_react3 = require("react"); +-function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function PeopleIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), +- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), +- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) + return kept + CUT; +}; +var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length }); @@ -7745,8 +7850,10 @@ diff --git a/client.js b/client.js + peek ? (0, import_react11.createElement)("span", { className: "bt-think-peek" }, peek) : null + ), + (0, import_react11.createElement)("div", { className: "bt-think-text" }, text2) -+ ); -+} + ); + } +-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 useFileMentions({ node, openFile, fileMentions, useTurnData }) { + const tail = useTurnData?.("turn-tail"); + const location2 = node.location; @@ -7756,18 +7863,36 @@ 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]); -+} + } +-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 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; -+} + } +-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 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; + return (0, import_react11.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react11.createElement)(BotMark, { bot, size: 22 })); -+} + } +-function ShareIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { + const roster = useRoster((value) => value); + const data = node.data; @@ -7805,8 +7930,27 @@ diff --git a/client.js b/client.js + }), + reasoning ? (0, import_react11.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react11.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles -+ ); -+} + ); + } +-function CloseIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-var 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 HeldReply({ held, roster, sessionId, actions, closing, children }) { + const bot = roster.byId[sessionId]; + const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to); @@ -7816,8 +7960,33 @@ diff --git a/client.js b/client.js + { className: "bt-held-reply", "data-state": held.status, "data-held": isApprovalWaiting(held.status) ? "" : void 0 }, + children, + closing ? (0, import_react11.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions, roster }) : null -+ ); -+} + ); + } +-var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); +-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); +-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); +-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); +-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); +-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); +-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); +-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); +-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); +-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); +-var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); +-var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); +-function PinMarkIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( +- "g", +- { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) +- ) +function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) { + if (!(texts2 ?? []).some((text2) => String(text2 ?? "").trim() !== "")) return null; + const lead = (0, import_react11.createElement)("span", { className: "bt-lead", title: name }, (0, import_react11.createElement)(PersonMark, { name })); @@ -7826,53 +7995,192 @@ diff --git a/client.js b/client.js + { className: "bt-group-msg bt-person-msg", "data-person": name }, + (0, import_react11.createElement)("span", { className: "bt-group-name" }, name), + (0, import_react11.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead }) -+ ); -+} -+function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -+ if (whole && bots.length === 1) { -+ return (0, import_react11.createElement)( -+ "div", -+ { className: "bt-event" }, -+ (0, import_react11.createElement)( -+ "button", -+ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -+ (0, import_react11.createElement)("span", null, prefix), -+ (0, import_react11.createElement)(BotMark, { bot: bots[0], size: 16 }), -+ (0, import_react11.createElement)("span", { className: "bt-event-name" }, bots[0].name) -+ ) -+ ); -+ } -+ const parts = []; -+ parts.push((0, import_react11.createElement)("span", { key: "p" }, prefix)); -+ bots.forEach((bot, index) => { -+ if (index > 0) parts.push((0, import_react11.createElement)("span", { key: `j${index}` }, joiner)); -+ parts.push((0, import_react11.createElement)( -+ "button", -+ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -+ (0, import_react11.createElement)(BotMark, { bot, size: 16 }), -+ bot.name -+ )); -+ }); -+ return (0, import_react11.createElement)("div", { className: "bt-event" }, parts); -+} -+var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); -+var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); -+function isHeldLine(room2, text2, bot) { -+ if (room2?.origin !== "face-to-face") return false; -+ const line2 = heldForm(text2); -+ if (!line2) return false; -+ return (room2.pendingApprovals ?? []).some((item) => { -+ if (item?.kind !== "face_room_message") return false; -+ if (bot !== void 0 && String(item.from ?? "").trim() !== "" && !sameName(item.from, bot.name)) return false; -+ const held = heldForm(item.text); -+ return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held)); -+ }); + ); } --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" }) +-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 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 +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( +- "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); +- } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( ++function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { ++ if (whole && bots.length === 1) { ++ return (0, import_react11.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)( ++ { className: "bt-event" }, ++ (0, import_react11.createElement)( ++ "button", ++ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, ++ (0, import_react11.createElement)("span", null, prefix), ++ (0, import_react11.createElement)(BotMark, { bot: bots[0], size: 16 }), ++ (0, import_react11.createElement)("span", { className: "bt-event-name" }, bots[0].name) ++ ) ++ ); ++ } ++ const parts = []; ++ parts.push((0, import_react11.createElement)("span", { key: "p" }, prefix)); ++ bots.forEach((bot, index) => { ++ if (index > 0) parts.push((0, import_react11.createElement)("span", { key: `j${index}` }, joiner)); ++ parts.push((0, import_react11.createElement)( ++ "button", ++ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, ++ (0, import_react11.createElement)(BotMark, { bot, size: 16 }), ++ bot.name ++ )); ++ }); ++ return (0, import_react11.createElement)("div", { className: "bt-event" }, parts); ++} ++var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); ++var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); ++function isHeldLine(room2, text2, bot) { ++ if (room2?.origin !== "face-to-face") return false; ++ const line2 = heldForm(text2); ++ if (!line2) return false; ++ return (room2.pendingApprovals ?? []).some((item) => { ++ if (item?.kind !== "face_room_message") return false; ++ if (bot !== void 0 && String(item.from ?? "").trim() !== "" && !sameName(item.from, bot.name)) return false; ++ const held = heldForm(item.text); ++ return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held)); ++ }); ++} +var HELD_SENDING_MS = 6e4; +function heldLineState(room2, { seq, botId, text: text2 } = {}, now = Date.now()) { + if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0; @@ -7889,7 +8197,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_react11.createElement)( -+ "div", + "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 + { className: "bt-event bt-not-sent", "data-flybot": "held-not-sent", role: "status" }, + (0, import_react11.createElement)(BotMark, { bot, size: 16 }), + (0, import_react11.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) @@ -7914,8 +8227,12 @@ diff --git a/client.js b/client.js + (0, import_react11.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ); } --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 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 remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -7958,7 +8275,14 @@ diff --git a/client.js b/client.js + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + if (person) { + return (0, import_react11.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-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_react11.createElement)( @@ -7969,12 +8293,25 @@ diff --git a/client.js b/client.js + ), + String(words ?? "").trim() === "" ? null : (0, import_react11.createElement)(Bubbles, { texts: [words], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react11.createElement)("span", { className: "bt-lead", title: person }, (0, import_react11.createElement)(PersonMark, { name: person })) }), + files.length ? (0, import_react11.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null -+ ); -+ } + ); + } +- return (0, import_react4.createElement)( + const mark = (0, import_react11.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react11.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react11.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react11.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) + { className: "bt-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_react11.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react11.createElement)( @@ -7985,10 +8322,21 @@ diff --git a/client.js b/client.js + ), + String(words ?? "").trim() === "" ? null : (0, import_react11.createElement)(Bubbles, { texts: [words], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react11.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null -+ ); + ); } --function MainStar() { -- return (0, import_react2.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; +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; @@ -7997,23 +8345,12 @@ diff --git a/client.js b/client.js +} +function GlobeMark() { + return (0, import_react11.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" }) ++ "svg", + { width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, + (0, import_react11.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react11.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) - ); - } --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 RemoteFiles({ files, plugin, botId }) { + const [busy, setBusy] = (0, import_react11.useState)(""); + const [error, setError] = (0, import_react11.useState)(""); @@ -8033,13 +8370,9 @@ diff --git a/client.js b/client.js + } finally { + setBusy(""); } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; -+ }; +- pending.answer({ answers: next }); + }; +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); + return (0, import_react11.createElement)( + "div", + { className: "bt-file-chips" }, @@ -8051,36 +8384,34 @@ diff --git a/client.js b/client.js + error ? (0, import_react11.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null + ); } --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 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 GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_react11.createElement)( -+ "div", + "div", +- { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) + { className: "bt-event" }, + (0, import_react11.createElement)("span", null, prefix), + room2 ? (0, import_react11.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react11.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react11.createElement)("span", null, name) : null ); } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; -- } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; +-function 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 MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { + if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; + const self = roster.byId[sessionId]; @@ -8095,40 +8426,79 @@ diff --git a/client.js b/client.js + (0, import_react11.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react11.createElement)(MemoryIcon), label) + ); } --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 -- ); +-function AnsweredCell({ node }) { +- const data = node.data ?? {}; +- const questions = Array.isArray(data.questions) ? data.questions : []; +- if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; +- } +- const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- key: question.id, +- question, +- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +- }))); +function PartLine({ time }) { + return (0, import_react11.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react11.createElement)("span", null, dayLabel(time)) : null); -+} + } +- +-// src/client/secret-card.js +-var import_react5 = require("react"); +-var MIN_VALUE = 4; +-var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), +- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) +-); +-var LockIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), +- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) +-); +-var WarnIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), +- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) +-); +-var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); +-var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); +-var scopeLabel = (scope, roster) => { +- if (scope === "all") return t("All Bots"); +- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); +- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); +var scrollParent = (node) => { + for (let at = node.parentElement; at; at = at.parentElement) { + if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; - } -- 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}`); ++ } + return null; -+}; + }; +-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; +-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +function EarlierParts({ sessionId, roster, actions, useUi }) { + const [view, setView] = (0, import_react11.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react11.useRef)(null); @@ -8220,19 +8590,11 @@ diff --git a/client.js b/client.js + } + default: + return null; - } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) ++ } + }), + (0, import_react11.createElement)(PartLine, { time: view.startedAt }) - ); - } -- --// 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 TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { + const roster = useRoster((value) => value); + const data = node.data; @@ -8304,7 +8666,8 @@ diff --git a/client.js b/client.js + onOpen: (bot) => actions.openExchange(selfId, bot.id), + whole: true + }); -+ } + } +- }); + if (source.kind === "schedule") { + const prompts = reminderPrompts(contentText(data.content)); + return (0, import_react11.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); @@ -8312,10 +8675,38 @@ diff --git a/client.js b/client.js + if (source.kind === "user-question-reply") return null; + return (0, import_react11.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); + } -+} + } +-function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { +- key: id, +- type: "button", +- role: "radio", +- "aria-checked": value === id, +- className: "bt-secret-seg", +- onClick: () => onChange(id) +- }, text)) +- ); +function NewDivider() { + return (0, import_react11.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react11.createElement)("span", null, t("New"))); -+} + } +-function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") +- ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null +- ); +function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { + const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + (0, import_react11.useEffect)(() => { @@ -8333,54 +8724,129 @@ diff --git a/client.js b/client.js + 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_react11.createElement)(NewDivider) : null; -+} -+function Nothing() { -+ return null; } --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" })); -+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([ -+ "list_bots", -+ "message_bot", -+ "stop_bot", -+ "create_bot", -+ "update_bot", -+ "archive_bot", -+ "list_models", -+ "bot_schedules", -+ "create_group", -+ "update_group", -+ "delete_group", -+ "post_to_group", -+ "ask_user", -+ "track_tasks", -+ "request_secret", -+ "list_secrets", -+ "group_relay", -+ "read_own_chat", -+ "read_group_chat", -+ "remember", -+ "forget", -+ "recall", -+ "read_browser", -+ "list_capabilities", -+ "save_capability", -+ "delete_capability", -+ "attach_capability", -+ "detach_capability", -+ "track_links", -+ "project_state", -+ "project_digest", -+ "meeting" -+]); -+var isSilentRegistration = (name) => typeof name === "string" && SILENT_REGISTRATION_TOOLS.has(name); -+var isHiddenNotice = (block) => block?.type === "tool-removal" && (HIDDEN_REMOVED_TOOLS.has(block.toolName) || isConnectorTool(block.toolName) || isPluginTool(block.toolName) || isSilentRegistration(block.toolName)) || block?.type === "tool-addition" && (isConnectorTool(block.toolName) || isPluginTool(block.toolName) || isSilentRegistration(block.toolName)); -+function isHiddenToolRemoval(node) { -+ const content = node?.data?.content; -+ if (!Array.isArray(content) || content.length === 0) return false; -+ return content.every(isHiddenNotice); +-function SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); +- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); +- const run = (promise) => { +- setBusy(true); +- setError(""); +- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); +- }; +- const cancel = () => run(actions.secretCancel(request.id)); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); +- }; +- const allow = () => { +- if (!busy) void run(actions.secretAllow(request.id)); +- }; +- const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { +- bot, +- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), +- detail: request.purpose, +- name: request.name, +- onClose: busy ? void 0 : cancel +- }), +- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, +- filling ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-body" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), +- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null +- ) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-note", id: noteId }, +- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), +- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), +- ".", +- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" +- ), +- warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-foot" }, +- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), +- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) +- ) +- ); ++function Nothing() { ++ return null; + } +-function SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), +- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( +- "p", +- { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) +- )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) +- ) +- ); ++var 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([ ++ "list_bots", ++ "message_bot", ++ "stop_bot", ++ "create_bot", ++ "update_bot", ++ "archive_bot", ++ "list_models", ++ "bot_schedules", ++ "create_group", ++ "update_group", ++ "delete_group", ++ "post_to_group", ++ "ask_user", ++ "track_tasks", ++ "request_secret", ++ "list_secrets", ++ "group_relay", ++ "read_own_chat", ++ "read_group_chat", ++ "remember", ++ "forget", ++ "recall", ++ "read_browser", ++ "list_capabilities", ++ "save_capability", ++ "delete_capability", ++ "attach_capability", ++ "detach_capability", ++ "track_links", ++ "project_state", ++ "project_digest", ++ "meeting" ++]); ++var isSilentRegistration = (name) => typeof name === "string" && SILENT_REGISTRATION_TOOLS.has(name); ++var isHiddenNotice = (block) => block?.type === "tool-removal" && (HIDDEN_REMOVED_TOOLS.has(block.toolName) || isConnectorTool(block.toolName) || isPluginTool(block.toolName) || isSilentRegistration(block.toolName)) || block?.type === "tool-addition" && (isConnectorTool(block.toolName) || isPluginTool(block.toolName) || isSilentRegistration(block.toolName)); ++function isHiddenToolRemoval(node) { ++ const content = node?.data?.content; ++ if (!Array.isArray(content) || content.length === 0) return false; ++ return content.every(isHiddenNotice); +} +function ContextCell({ node, useRoster }) { + useRoster?.((roster) => (roster.pluginTools ?? []).join("\n")); @@ -8395,14 +8861,8 @@ diff --git a/client.js b/client.js + if (parts.length === 0) return null; + return (0, import_react11.createElement)("div", { className: "bt-event" }, parts.join(" · ")); } --function PeopleIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), -- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), -- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) -- ); +-function 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 }); +var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +function clockLabel(time, now = Date.now()) { + const date = new Date(time); @@ -8414,22 +8874,64 @@ diff --git a/client.js b/client.js + const sameYear = date.getFullYear() === new Date(now).getFullYear(); + return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; } --function 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 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 nextTurnStart(turns, turn) { + let next; + for (const key in turns) { + const candidate = Number(key); + if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; -+ } + } +- return 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 next === void 0 ? void 0 : turns[next].first; } --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 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")) +- ) - ); +function openingTurn(turns, firstTurns, hiddenTurns) { + if (!turns || !firstTurns) return null; @@ -8440,11 +8942,34 @@ diff --git a/client.js b/client.js + const earlier = listed.filter((entry) => entry < turn); + return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; } --function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) +-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 - ); +var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +function movedOnSince(visit, turns, hiddenTurns) { @@ -8455,143 +8980,130 @@ diff --git a/client.js b/client.js + } + return false; } --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 settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; -+function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -+ const turn = location2.turn; -+ const time = location2.end.time; -+ (0, import_react11.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_react11.createElement)( -+ import_react11.Fragment, -+ null, -+ label ? (0, import_react11.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -+ fresh ? (0, import_react11.createElement)(NewDivider) : null - ); - } --function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-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 settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; ++function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { ++ const turn = location2.turn; ++ const time = location2.end.time; ++ (0, import_react11.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_react11.createElement)( ++ import_react11.Fragment, ++ null, ++ label ? (0, import_react11.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, ++ fresh ? (0, import_react11.createElement)(NewDivider) : null + ); + } +-function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); +- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!ready) return; +- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { +- if (ok) { +- setValue(""); +- onDone(); +- } +- }); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-set-row bt-set-paste bt-secret-add" }, +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) +- ) +- ); +function Shimmer({ children }) { + return (0, import_react11.createElement)("span", { className: "bt-shimmer" }, children); } --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); --var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) -- ); +-function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; +var PHRASES = { + thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], + working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] - }; --var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; --function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -- ); --} --var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); --var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); --var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); --var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); --var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); --var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); --var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); --var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); --var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); --var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); --var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); --var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); --var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); --var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); --var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); --function PinMarkIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( -- "g", -- { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) -- ) -- ); --} --var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); --var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); --var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); --var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); --var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); --var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); --var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); --var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); --var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); --var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); --var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); --var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); --var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; -- --// src/client/questions.js --var import_react4 = require("react"); --var keyLetter = (index) => String.fromCharCode(65 + index); --var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); --var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; --function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); -- pick2.current = onPick; -- (0, import_react4.useEffect)(() => { -- if (count === 0) return void 0; -- const onKey = (event) => { -- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -- const index = event.key.toUpperCase().charCodeAt(0) - 65; -- if (index >= 0 && index < count) { -- event.preventDefault(); -- pick2.current(index); -- } else if (index === count && ownRef.current) { -- event.preventDefault(); -- ownRef.current.focus(); -- } -- }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); --} --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_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; ++}; +var TOOL_PHRASES = [ + [/web_?search|search_?web/i, "Searching online"], + [/fetch|browse|browser|web/i, "Reading a page"], @@ -8617,75 +9129,11 @@ diff --git a/client.js b/client.js + case "message_bot": { + const target = findBot(arg("to")); + return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; - } -- 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(); ++ } + case "stop_bot": { + const target = findBot(arg("bot")); + return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; - } -- }) : null; -- const rows = options.map((option, optionIndex) => { -- const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( -- "button", -- { -- key: option.label, -- type: "button", -- className: "bt-q-opt", -- "aria-pressed": multi ? selected : void 0, -- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -- onClick: () => choose(option.label) -- }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( -- "span", -- { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null -- ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null -- ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); -- } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( -- "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) -- ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); ++ } + case "create_bot": + return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; + case "update_bot": { @@ -8708,106 +9156,103 @@ 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 (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 -- ); ++ } } --function SettledQuestion({ question, answer }) { -- const options = optionsOf(question); -- const prompt = question?.question ?? ""; -- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -- if (chosen.length === 0) { -- return (0, import_react4.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -- ) -- ); -- } -- return (0, import_react4.createElement)( -- "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)) -- ); -- })) -- ); +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +-`; +- +-// src/client/turn-reasoning.js +-function turnReasoning(steps, current) { +- const members = steps.length ? steps : [current]; +- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; +- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); +function workPhrase(tool) { + if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); } --function 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 }); +-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); - }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( +function workText(tool, step) { + return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; - } --function 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 useLiveNote(actions, sessionId, id, entry) { + const key = entry ? JSON.stringify(entry) : ""; + (0, import_react11.useEffect)(() => { @@ -8852,8 +9297,16 @@ diff --git a/client.js b/client.js + const room2 = roster.roomsById[sessionId]; + return (0, import_react11.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 }) +- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( + { className: "bt-group-replies" }, + replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; @@ -8901,7 +9354,24 @@ diff --git a/client.js b/client.js + } + if (reply.kind === "approval" || reply.kind === "working") { + return (0, import_react11.createElement)( -+ "div", + "div", +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) +- ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); + { key: `${reply.botId}-${index}`, className: "bt-event" }, + (0, import_react11.createElement)( + "button", @@ -8933,42 +9403,107 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react11.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 }); -+ }) + }) ); } --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 }); -+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 = (call4) => { -+ const id = call4.callId ?? call4; +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +-} +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; +- } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); +- }); +- }; +- const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); +- link.click(); +- setTimeout(() => URL.revokeObjectURL(url), 1e3); ++var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); ++var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); ++function callTree(root) { ++ const calls = []; ++ const seen = /* @__PURE__ */ new Set(); ++ const visit = (call4) => { ++ const id = call4.callId ?? call4; + if (seen.has(id)) return; + seen.add(id); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); -+ }; + }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { +- close(); +- run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( +- "div", +- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, +- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { +- close(); +- react(emoji); +- } }, emoji)) +- ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, +- item(t("Copy"), CopyIcon, copy2), +- item(t("Download"), DownloadIcon, download2) +- ), document.body) : null +- ); + visit(root); + return calls; } --function AnsweredCell({ node }) { -- const data = node.data ?? {}; -- const questions = Array.isArray(data.questions) ? data.questions : []; -- if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; -- } -- const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { -- key: question.id, -- question, -- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -- }))); +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +- "details", +- { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +- ); +function useShownCards(actions, sessionId, callIds) { + const key = JSON.stringify(callIds); + (0, import_react11.useLayoutEffect)(() => { @@ -8978,59 +9513,15 @@ diff --git a/client.js b/client.js + return () => actions.noteCards(sessionId, ids, -1); + }, [sessionId, key]); } -- --// src/client/secret-card.js --var import_react5 = require("react"); --var MIN_VALUE = 4; --var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) --); --var LockIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) --); --var WarnIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) --); --var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); --var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); --var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); -- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); --}; --var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; --function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { -- return (0, import_react5.createElement)("input", { -- className: "bt-secret-input", -- type: MASK_BY_CSS ? "text" : "password", -- "data-masked": MASK_BY_CSS ? "" : void 0, -- value, -- autoFocus, -- autoComplete: MASK_BY_CSS ? "off" : "new-password", -- autoCorrect: "off", -- autoCapitalize: "off", -- spellCheck: false, -- "data-1p-ignore": "", -- "data-lpignore": "true", -- "data-bwignore": "", -- "data-form-type": "other", -- "aria-label": label, -- placeholder: t("Paste or type the key"), -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- onEnter?.(); +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; +- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; +- }, [fileMentions, openFile, seq, tail, turn]); +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; @@ -9075,8 +9566,7 @@ diff --git a/client.js b/client.js + const name = named[2]; + const room2 = roster.rooms.find((entry) => entry.name === name); + return (0, import_react11.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 !== call4.callId) return (0, import_react11.createElement)("span", { className: "bt-q-gone", hidden: true }); @@ -9109,23 +9599,41 @@ diff --git a/client.js b/client.js + return null; + } } --function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, -- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -- options.map(([id, text]) => (0, import_react5.createElement)("button", { -- key: id, -- type: "button", -- role: "radio", -- "aria-checked": value === id, -- className: "bt-secret-seg", -- onClick: () => onChange(id) -- }, text)) -- ); +-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { -+ const roster = useRoster((value) => value); + const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- const running = data.status === "running"; +- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); +- if (hidden) return null; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; +- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true +- }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +- ); + const root = node.data.root; + const turn = node.location?.turn?.turn ?? root.turn; + const hidden = isHiddenTurn(roster, sessionId, turn); @@ -9140,19 +9648,32 @@ diff --git a/client.js b/client.js + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react11.createElement)(import_react11.Fragment, null, drawn.map((call4, index) => (0, import_react11.createElement)(import_react11.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); } --function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head bt-secret-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), -- (0, import_react5.createElement)( +-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { +- if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( - "div", -- { className: "bt-q-text" }, -- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), -- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") -- ), -- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null -- ); +- { className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, +- (0, import_react6.createElement)("span", null, prefix), +- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) +- ) +- ); +- } +- const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( +- "button", +- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- bot.name +- )); +- }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -9161,63 +9682,22 @@ diff --git a/client.js b/client.js + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; } --function SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); -- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -- const run = (promise) => { -- setBusy(true); -- setError(""); -- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -- }; -- const cancel = () => run(actions.secretCancel(request.id)); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -- }; -- const allow = () => { -- if (!busy) void run(actions.secretAllow(request.id)); -- }; -- const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) +- ); +- return (0, import_react6.createElement)( - "div", -- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { -- bot, -- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -- detail: request.purpose, -- name: request.name, -- onClose: busy ? void 0 : cancel -- }), -- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, -- filling ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-body" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), -- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -- ) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-note", id: noteId }, -- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), -- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), -- ".", -- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" +- { className: "bt-group-msg" }, +- (0, import_react6.createElement)( +- "span", +- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, +- bot.name, +- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null - ), -- warn, -- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-foot" }, -- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), -- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) -- ) +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) - ); + +// src/client/remote-approval-wait.js @@ -9227,24 +9707,12 @@ diff --git a/client.js b/client.js + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; } --function SettledSecret({ bot, request, roster }) { -- const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( +-function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( - "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( -- "p", -- { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -- )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) -- ) +- { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null - ); + +// src/client/activity-row-host.js @@ -9256,8 +9724,19 @@ diff --git a/client.js b/client.js + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); } --function 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 MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- const target = other && other.id !== self?.id ? other : self; +- if (!target) return null; +- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) +- ); + +// src/client/activity-row.js +var import_react12 = require("react"); @@ -9270,15 +9749,8 @@ diff --git a/client.js b/client.js + const hours = Math.floor(minutes / 60); + return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; } --function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -- const bot = roster.byId[sessionId]; -- if (!bot) return null; -- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); -- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); -- if (done && /already saved/.test(contentText(content))) { -- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -- } -- return null; +-function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -9291,16 +9763,11 @@ diff --git a/client.js b/client.js + }, [shown, at]); + return shown; } --function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const roster = useRoster((value) => value); -- const shown = useCards?.((state) => state[sessionId]) ?? NONE; -- const loading = useSession?.(historyLoading) === true; -- const bot = roster.byId[sessionId]; -- if (!bot || currentPart(bot) !== sessionId) return null; -- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); -- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); -- if (waiting.length === 0) return null; -- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; +- } +- return null; +function findHost(where) { + if (where !== TAIL_HOST) return findRunningHostIn(document); + const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]"); @@ -9314,7 +9781,26 @@ diff --git a/client.js b/client.js + let top = -1; + for (const node of document.querySelectorAll(`[data-chat-flow-kind${kind ? `="${kind}"` : ""}][data-chat-turn]`)) top = Math.max(top, Number(node.dataset.chatTurn)); + return top; -+}; + }; +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +function useReplyAfterSend(remote, sending) { + const sendTurn = (0, import_react12.useRef)(-1); + const wasSending = (0, import_react12.useRef)(false); @@ -9339,53 +9825,142 @@ diff --git a/client.js b/client.js + }; + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); -+ }); + }); +- }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + check(); -+ return () => { + return () => { +- alive.current = false; + cancelAnimationFrame(frame); + observer.disconnect(); -+ }; -+ }, [remote, sending]); -+ return replied; - } --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)( + }; +- }, [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-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), -- (0, import_react5.createElement)( +- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.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 -- )) +- { 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 - ), -- 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")) -- ) +- rows.map((row, index) => { +- const key = rows.length - index; +- switch (row.kind) { +- case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null +- )); +- case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); +- case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); +- case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); +- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; +- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { +- if (selfId && other.id) actions.openExchange(selfId, other.id); +- }, whole: true }); +- } +- default: +- return null; +- } +- }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) - ); -+function useRunningHost(active, where = RUNNING_HOST) { -+ const [host, setHost] = (0, import_react12.useState)(null); -+ (0, import_react12.useEffect)(() => { -+ if (!active) { -+ setHost(null); ++ }, [remote, sending]); ++ return replied; + } +-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- const deferred = source.kind === "bot" && source.deferred === true; +- const relayed = isRelayContext(data); +- const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { +- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); +- }, [relayed, sessionId, turn, source.senderSessionId]); +- const order = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; +- }); +- const peers = useTurnClock((state) => { +- if (!relayed) return ""; +- const turns = state[sessionId] ?? {}; +- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const hiddenTurns = roster.exchangeTurns[sessionId]; +- const unreadOpening = useTurnClock((state) => { +- const first = openingTurn(state[sessionId], order, hiddenTurns); +- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; +- }); +- const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; +- function triggerBody() { +- if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; +- if (source.role === "secret" || deferred) return null; +- if (source.role === "post") { +- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; +- const room2 = roster.roomsById[sessionId]; +- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); +- } +- if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); +- } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); +- const run = relayed ? commRunOf( +- order.split(",").filter(Boolean).map(Number), +- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +- turn, +- hiddenTurns +- ) : null; +- if (run === "folded") return null; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); +- const bots = [sender]; +- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { +- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); ++function useRunningHost(active, where = RUNNING_HOST) { ++ const [host, setHost] = (0, import_react12.useState)(null); ++ (0, import_react12.useEffect)(() => { ++ if (!active) { ++ setHost(null); + return void 0; + } + let frame = 0; @@ -9405,17 +9980,7 @@ diff --git a/client.js b/client.js + }; + }, [active, where]); + return host; - } --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); ++} +function useRowEntrances(replacing) { + const check = (0, import_react12.useRef)(replacing); + check.current = replacing; @@ -9437,30 +10002,17 @@ diff --git a/client.js b/client.js + column = next; + if (column) observer.observe(column, { childList: true, subtree: 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 -- ); +- return (0, import_react6.createElement)(EventLine, { +- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), +- bots, +- onOpen: (bot) => actions.openExchange(selfId, bot.id), +- whole: true + if (!column) return; + const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; + let last = -1; + items.forEach((item, index) => { + if (seen.has(item)) last = index; -+ }); + }); + const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); + for (const item of fresh) seen.add(item); + if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; @@ -9480,16 +10032,7 @@ diff --git a/client.js b/client.js + observer.disconnect(); + }; + }, []); - } --function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { -- if (!open) setConfirm(false); -- }, [open]); -- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( ++} +function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { + const roster = useRoster((value) => value); + const running = useSessionStatus((map) => map.get(sessionId)?.running === true); @@ -9528,7 +10071,10 @@ diff --git a/client.js b/client.js + if (wait <= 0) { + make(); + return void 0; -+ } + } +- if (source.kind === "schedule") { +- const prompts = reminderPrompts(contentText(data.content)); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); + const timer = setTimeout(make, wait); + return () => clearTimeout(timer); + }, [wantKey, shown]); @@ -9541,7 +10087,10 @@ diff --git a/client.js b/client.js + if (visible) { + setPhase("in"); + return void 0; -+ } + } +- if (source.kind === "user-question-reply") return null; +- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); +- } + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -9585,237 +10134,24 @@ diff --git a/client.js b/client.js + if (!host || phase === null || !shown) return null; + const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + return (0, import_react_dom2.createPortal)((0, import_react12.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)( ++ "div", + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react12.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react12.createElement)( + "span", + { className: "bt-typing-text" }, + (0, import_react12.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")) ++ "span", + { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + (0, import_react12.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null - ) -- ) : null -- ); --} --function AddSecret({ actions, run, onDone }) { -- const [name, setName] = (0, import_react5.useState)(""); -- const [purpose, setPurpose] = (0, import_react5.useState)(""); -- const [value, setValue] = (0, import_react5.useState)(""); -- 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 SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); -- const secrets = roster.secrets ?? []; -- return [ -- (0, import_react5.createElement)( -- "p", -- { key: "about", className: "bt-secret-about" }, -- t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), -- t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), -- t(" before anything goes to a model.") -- ), -- (0, import_react5.createElement)( -- Section, -- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -- ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -- ]; --} --var SECRET_CSS = ` --.bt-secret-head{align-items:center;gap:10px} --.bt-secret-who{position:relative;flex:none;display:inline-flex} --.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} --.bt-secret-lock svg{width:11px;height:11px} --.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} --.bt-secret-body{display:flex;flex-direction:column;gap:8px} --.bt-secret-field{align-items:center;min-height:36px} --.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} --.bt-secret-input[data-masked]{-webkit-text-security:disc} --.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} --.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} --.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-secret-glide[data-at="1"]{transform:translateX(100%)} --.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} --.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} --.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --/* A key card asks the user to act, so the process group carrying it stays open in every -- work-details mode (same override as .bt-group-replies in styles.js). */ --[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} --[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} --.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} --.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} --@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} --.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} --.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} --.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} --.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} --.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} --.bt-secret-done .bt-q-head{align-items:center} --.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} --.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} --.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} --@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} --@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} --.bt-secret-event{display:inline-flex;align-items:center;gap:5px} --.bt-secret-dock{flex-direction:column;align-items:center} --.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} --.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} --.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} --.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} --.bt-secret-faces{display:inline-flex} --.bt-secret-faces>*+*{margin-left:-6px} --.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} --.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} --.bt-secret-radios{display:flex;gap:16px;font-size:13px} --.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} --.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} --.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} --.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} --.bt-secret-bot svg{width:13px;height:13px} --.bt-secret-inline{display:flex;gap:8px;align-items:center} --.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} --.bt-secret-row-actions .bt-set-hint{flex:1} --.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} --.bt-secret-danger svg{width:13px;height:13px} --.bt-secret-add .bt-input{height:34px} --@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} --`; -- --// 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"); ++ ) ++ ) + ), 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 -- ); -- }) -- ); --} --var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; --function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (refs.some((ref) => ref.current?.contains(event.target))) return; -- close(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -- } +-function NewDivider() { +- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); ++ +// src/client/dsh-offer.js +var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; @@ -9824,7 +10160,24 @@ diff --git a/client.js b/client.js + if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; + if (dsh.kind === "npm") return true; + return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); -+} + } +-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- const own = state[sessionId]?.[turn]; +- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; +- }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; + +// src/client/overlay-hooks.js +var import_react13 = require("react"); @@ -9841,81 +10194,33 @@ diff --git a/client.js b/client.js + return () => { + escapeLayers.splice(escapeLayers.indexOf(ref), 1); + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); - }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); ++ }; + }, []); -+} + } +-function Nothing() { +- return null; +function useDialogFocus(ref) { + (0, import_react13.useEffect)(() => { + const before = document.activeElement; + ref.current?.focus?.({ preventScroll: true }); - return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); ++ return () => { + const active = document.activeElement; + const stillInDialog = active === ref.current || ref.current?.contains?.(active) === true; + const noFocusHeld = !active || active === document.body || active.isConnected === false; + if (before && typeof before.focus === "function" && before.isConnected && (stillInDialog || noFocusHeld || active === before)) before.focus({ preventScroll: true }); - }; -- }, [open]); ++ }; + }, []); } --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); -- const close = () => setPanel(null); -- useDismiss(panel !== null, close, [tools, pop]); -- const open = (kind, event) => { -- if (panel === kind) { -- close(); -- return; -- } -- const rect = event.currentTarget.getBoundingClientRect(); -- const below = rect.bottom + 220 < window.innerHeight; -- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -- setPanel(kind); -- }; -- const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { -- setCopied(true); -- setTimeout(() => setCopied(false), 1e3); -- }); -- }; -- const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -- link.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); -- }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -- close(); -- run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( -- "div", -- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, -- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -- close(); -- react(emoji); -- } }, emoji)) -- ), document.body) : null, -- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, -- item(t("Copy"), CopyIcon, copy2), -- item(t("Download"), DownloadIcon, download2) -- ), document.body) : null -- ); +-var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +-function clockLabel(time, now = Date.now()) { +- const date = new Date(time); +- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); +- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); +- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); +- if (days === 0) return t("Today {time}", { time: clock4 }); +- if (days === 1) return t("Yesterday {time}", { time: clock4 }); +- const sameYear = date.getFullYear() === new Date(now).getFullYear(); +- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react13.useState)(280); + (0, import_react13.useLayoutEffect)(() => { @@ -9924,12 +10229,13 @@ diff --git a/client.js b/client.js + }, []); + return paneLeft; } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( -- "details", -- { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +-function nextTurnStart(turns, turn) { +- let next; +- for (const key in turns) { +- const candidate = Number(key); +- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +- } +- return next === void 0 ? void 0 : turns[next].first; + +// src/client/member-status.js +var import_react14 = require("react"); @@ -9952,49 +10258,141 @@ diff --git a/client.js b/client.js + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; + }); -+} + } +-function openingTurn(turns, firstTurns, hiddenTurns) { +- if (!turns || !firstTurns) return null; +- const start = nextTurnStart(turns, -Infinity); +- if (start === void 0) return null; +- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); +- const listed = firstTurns.split(",").map(Number); +- const earlier = listed.filter((entry) => entry < turn); +- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); -+} + } +-var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { +- const turn = location2.turn; +- const time = location2.end.time; +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "end", time); +- }, [sessionId, turn, time]); +- const label = useTurnClock((state) => { +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- if (typeof visit?.seen !== "number" || time > visit.seen) return false; +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start > visit.seen && start <= visit.entered; +- }); +- if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null +- ); +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); + return { rows, rest: busy.filter((entry) => !rows.includes(entry)) }; -+} + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +function botGroupWork(botId, work, roster) { + return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => { + const entry = entries2.find((item) => item.botId === botId); + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; + }); -+} -+var moreRooms = (count) => count === 1 ? t("1 more room") : t("{count} more rooms", { count }); -+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} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; -+ return { waiting, doing }; -+} + } +-var PHRASES = { +- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] +-}; +-var TOOL_PHRASES = [ +- [/web_?search|search_?web/i, "Searching online"], +- [/fetch|browse|browser|web/i, "Reading a page"], +- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], +- [/^(read|view|cat)\b|read_/i, "Opening a file"], +- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], +- [/write|edit|patch|apply|replace/i, "Writing changes"], +- [/image|photo|draw/i, "Working on an image"] +-]; +-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; +-function toolActivity(name, arg, findBot, self) { +- switch (name) { +- case "ask_user": +- return null; +- case "request_secret": +- return null; +- case "list_secrets": +- return { label: t("Checking the keys"), state: "searching" }; +- case "ask_user_question": +- return { label: t("Waiting for your answer"), state: "alert" }; +- case "group_relay": +- return { relay: true, state: "orbit" }; +- case "message_bot": { +- const target = findBot(arg("to")); +- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; +- } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +- } +- case "create_bot": +- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; +- } +- case "list_bots": +- return { label: t("Checking the team"), state: "searching" }; +- case "read_group_chat": +- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; +- case "read_own_chat": +- return { label: t("Looking back through the chat"), state: "searching" }; +- case "remember": +- return { label: t("Saving to memory"), state: "working" }; +- case "forget": +- return { label: t("Updating memory"), state: "working" }; +- case "recall": +- return { label: t("Checking memory"), state: "searching" }; +- default: +- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; +- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; +- return { label: phrase ? t(phrase) : null, state: toolState(name) }; +- } ++var moreRooms = (count) => count === 1 ? t("1 more room") : t("{count} more rooms", { count }); ++function summaryText(busy) { ++ const asks = busy.filter((entry) => entry.state === "ask"); ++ 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} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; ++ return { waiting, doing }; + } +-function workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +function Face({ entry, size }) { + return (0, import_react14.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react14.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) - ); ++ ); } --function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -- const tail = useTurnData?.("turn-tail"); -- const location2 = node.location; -- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -- const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { -- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -- }, [fileMentions, openFile, seq, tail, turn]); +-function useLiveNote(actions, sessionId, id, entry) { +- const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { +- if (!key) return void 0; +- actions.noteLive(sessionId, id, JSON.parse(key)); +- return () => actions.noteLive(sessionId, id, null); +- }, [sessionId, id, key]); +var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +function WorkRow({ bot, tool, step, label, at, now, onOpen }) { + const text2 = `${label} · ${workText(tool, step)}`; @@ -10007,39 +10405,41 @@ diff --git a/client.js b/client.js + elapsed ? (0, import_react14.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null + ); } --function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const steps = useTurnSteps(String(data.turn)); -- const contexts = useTurnContext(String(data.turn)); -- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); -- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); -- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); -- const running = data.status === "running"; -- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); -- if (hidden) return null; -- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; -- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- relays.map((relay) => { -- const source = relay.source; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { -- key: relay.seq, -- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -- bots: [sender], -- onOpen: () => { -- if (sender.id) actions.openExchange(selfId, sender.id); -- }, -- whole: true -- }); -- }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +- if (done) return void 0; +- let stop = false; +- let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +- } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); +- }; +- void tick(); +- return () => { +- stop = true; +- clearTimeout(timer); +- }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { +- try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; +- } +- } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -10055,15 +10455,13 @@ diff --git a/client.js b/client.js + doing ? (0, import_react14.createElement)(Shimmer, null, doing) : null + ), + elapsed ? (0, import_react14.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null - ); - } --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( ++ ); ++} +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); -+ const room2 = roster.roomsById[sessionId]; + const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( + const bot = room2 ? void 0 : roster.byId[sessionId]; + const ownChat = bot !== void 0 && work.bots?.[bot.id] !== void 0 && work.bots[bot.id].roomId === void 0; + const busy = room2 ? busyMembers(room2, work, roster) : []; @@ -10077,36 +10475,76 @@ diff --git a/client.js b/client.js + const { rows: rows2, rest } = splitBusy(busy); + const open = () => actions.showDetails(sessionId); + return (0, import_react14.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) -- ) ++ "div", + { ref, className: "bt-mw-dock-stack" }, + rows2.map((entry) => (0, import_react14.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })), + rest.length > 0 ? (0, import_react14.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null - ); - } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); -- }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); ++ ); ++ } + const rows = groups.slice(0, ROWS_MAX); + const more = groups.length - rows.length; + return (0, import_react14.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)( +- "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); +- }) + { ref, className: "bt-mw-dock-stack" }, + rows.map((item) => (0, import_react14.createElement)(WorkRow, { + key: item.room.id, @@ -10123,13 +10561,22 @@ diff --git a/client.js b/client.js + { className: "bt-mw-row bt-mw-line bt-mw-static" }, + (0, import_react14.createElement)("span", { className: "bt-mw-row-text" }, moreRooms(more)) + ) : null -+ ); + ); } --function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { -- const lead = (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) +-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +-function callTree(root) { +- const calls = []; +- const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; +- if (seen.has(id)) return; +- seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); +- }; +- visit(root); +- return calls; +function useFlowColumn(ref, active, key) { + (0, import_react14.useLayoutEffect)(() => { + const node = ref.current; @@ -10146,7 +10593,15 @@ diff --git a/client.js b/client.js + for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); + return () => observer.disconnect(); + }, [active, key]); -+} + } +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +function useNow(active) { + const [now, setNow] = (0, import_react14.useState)(Date.now); + (0, import_react14.useEffect)(() => { @@ -10155,7 +10610,82 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [active]); + return now; -+} + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +- const self = roster.byId[sessionId]; +- switch (tool) { +- case "message_bot": { +- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "stop_bot": { +- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "create_bot": { +- const created = findBot(arg("name")); +- if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); +- } +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; +- } +- case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); +- case "create_group": +- case "update_group": +- case "delete_group": +- case "post_to_group": { +- const verbs = { +- create_group: ["Created group", "Couldn't create the group"], +- update_group: ["Updated group", "Couldn't update the group"], +- delete_group: ["Deleted group", "Couldn't delete the group"], +- post_to_group: ["Posted in", "Couldn't post in the group"] +- }[tool]; +- if (!done) return null; +- const result = contentText(call2.content); +- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); +- const name = named[2]; +- const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); +- } +- case "ask_user": { +- const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- } +- case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); +- case "ask_user_question": { +- if (!done) return null; +- let parsed = null; +- try { +- parsed = JSON.parse(contentText(call2.content)); +- } catch { +- parsed = null; +- } +- const answer = parsed?.answers?.[0]; +- const question = argValue("questions")?.[0]; +- if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- } +- case "read_group_chat": { +- if (!done) return null; +- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +- } +- case "remember": +- case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; +- default: +- return null; +var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; +var alwaysChoices = (approval, bot) => bot?.remote ? [] : [ + ["always-bot", "Always allow for this Bot"], @@ -10185,8 +10715,7 @@ diff --git a/client.js b/client.js + { className: "bt-mw-actions" }, + (0, import_react14.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) + ) - ); -- return (0, import_react6.createElement)( ++ ); +} +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react14.useState)(false); @@ -10197,22 +10726,14 @@ diff --git a/client.js b/client.js + (0, import_react14.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_react14.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); -+ } + } + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); + void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); + }; + return (0, import_react14.createElement)( - "div", -- { className: "bt-group-msg" }, -- (0, import_react6.createElement)( -- "span", -- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, -- bot.name, -- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null -- ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ++ "div", + { className: "bt-mw-member" }, + (0, import_react14.createElement)( + "div", @@ -10220,34 +10741,29 @@ diff --git a/client.js b/client.js + (0, import_react14.createElement)("span", null, (0, import_react14.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react14.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) - ); ++ ); } --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 ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +- const roster = useRoster((value) => value); +- const root = node.data.root; +- const turn = node.location?.turn?.turn ?? root.turn; +- const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); +- const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); +- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; +- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); +- if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +function useMemberWork(room2, useWork) { + const work = useWork?.((value) => value) ?? NO_WORK; + const entries2 = room2 ? memberWork(room2, work) : []; + const now = useNow(entries2.some((entry) => entry.state !== "idle")); + return { entries: entries2, now }; } --function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -- const self = roster.byId[sessionId]; -- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -- const other = team || !scope ? void 0 : findBot(scope); -- const target = other && other.id !== self?.id ? other : self; -- if (!target) return null; -- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) -- ); +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} @@ -10282,7 +10798,17 @@ diff --git a/client.js b/client.js +@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} +@media (prefers-reduced-motion:reduce){.bt-mw-row{animation:none}} +`; -+ + +-// src/client/activity-row.js +-var import_react7 = require("react"); +-var import_react_dom2 = require("react-dom"); +-var PHRASE_HOLD_MS = 800; +-var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +// src/shared/task-board.js +var COLUMNS = ["not-started", "in-progress", "done"]; +var PRIORITIES = ["urgent", "high", "normal", "low"]; @@ -10335,8 +10861,21 @@ diff --git a/client.js b/client.js + if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; + return Math.max(0, Math.min(100, Math.round(value))); } --function 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 useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +- } +- let frame = 0; +- const find = () => { +- frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); +- setHost((prev) => prev === node ? prev : node); +- }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(find); + +// src/client/inbox.js +var import_react15 = require("react"); @@ -10391,13 +10930,46 @@ diff --git a/client.js b/client.js + }).catch(() => { + }).finally(() => { + if (!stopped) timer = setTimeout(load, POLL_MS3); -+ }); + }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); + load(); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); +- }; +- }, [active]); +- return host; +-} +-function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); +- check.current = replacing; +- (0, import_react7.useEffect)(() => { +- const seen = /* @__PURE__ */ new WeakSet(); +- const started = Date.now(); +- let column = null; +- let frame = 0; +- const settle = (event) => { +- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; +- delete event.currentTarget.dataset.btEnter; +- event.currentTarget.removeEventListener("animationend", settle); + stopped = true; + clearTimeout(timer); + taskListeners.delete(setTasks); -+ }; + }; +- const scan = () => { +- frame = 0; +- const next = document.querySelector("[data-chat-flow]"); +- if (next !== column) { +- observer.disconnect(); +- column = next; +- if (column) observer.observe(column, { childList: true, subtree: true }); +- } +- if (!column) return; +- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; +- let last = -1; +- items.forEach((item, index) => { +- if (seen.has(item)) last = index; + }, []); + return tasks; +} @@ -10415,7 +10987,15 @@ diff --git a/client.js b/client.js + workflowListeners.add(setWorkflows); + if (workflowPoll === null && typeof actions?.workflowState === "function") { + const load = () => Promise.resolve(actions.workflowState()).then(publishWorkflows, () => { -+ }); + }); +- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); +- for (const item of fresh) seen.add(item); +- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; +- for (const item of fresh) { +- if (items.indexOf(item) < last) continue; +- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); +- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; +- item.addEventListener("animationend", settle); + load(); + workflowPoll = setInterval(load, POLL_MS3); + } @@ -10424,14 +11004,13 @@ diff --git a/client.js b/client.js + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; -+ } -+ }; + } + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); + }, []); + return workflows; - } --var scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; ++} +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -10449,29 +11028,9 @@ diff --git a/client.js b/client.js + if (question == null || typeof question !== "object" || roster.roomsById?.[key]) continue; + if (question.question !== text2) continue; + if (key === bot.id || key === currentPart(bot)) return bot.id; - } -- return null; ++ } + return void 0; - }; --function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); -- const load = (cursor) => { -- if (busy.current) return; -- busy.current = true; -- setView((current) => ({ ...current, loading: true, failed: false })); -- actions.earlier(sessionId, cursor).then((page) => { -- busy.current = false; -- if (!alive.current) return; -- const scroller = top.current && scrollParent(top.current); -- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -- }, () => { -- busy.current = false; -- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); ++}; +var placeKey = (botId, roomId) => `${botId}|${roomId ?? ""}`; +var partOf = (roster, chat) => roster.roomsById?.[chat] || !roster.byId?.[chat] ? chat : currentPart(roster.byId[chat]); +var runningIn = (work, botId, roomId) => roomId === void 0 ? work.bots?.[botId]?.roomId === void 0 ? work.bots?.[botId] : void 0 : (work.rooms?.[roomId] ?? []).find((entry) => entry.botId === botId) ?? (work.bots?.[botId]?.roomId === roomId ? work.bots[botId] : void 0); @@ -10521,7 +11080,11 @@ diff --git a/client.js b/client.js + chat: approval.roomId ?? bot.id, + approval, + ...people(roster, bot.id) -+ }); + }); +- scan(); +- return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + } + for (const [key, question] of Object.entries(roster.questions ?? {})) { + if (question == null || typeof question !== "object") continue; @@ -10582,13 +11145,8 @@ diff --git a/client.js b/client.js + place: placeOf(roster, roomId), + chat: roomId ?? bot.id, + ...people(roster, bot.id) - }); - }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); -- return () => { -- alive.current = false; ++ }); ++ }; + for (const [botId, entry] of Object.entries(work.bots ?? {})) running(botId, entry, entry.roomId); + for (const [roomId, entries2] of Object.entries(work.rooms ?? {})) for (const entry of entries2) running(entry.botId, entry, roomId); + const lastEnded = /* @__PURE__ */ new Map(); @@ -10630,27 +11188,7 @@ diff --git a/client.js b/client.js + task, + ...people(roster, task.workBotId ?? bot.id) }; -- }, [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); +- }, []); + if (task.status === "failed") { + if (fresh(task.updatedAt)) needs.push({ ...card, status: "failed", reason: lastWhy(task) }); + } else if (task.status === "result-ready") needs.push({ ...card, status: "result", reason: task.result?.summary ?? "" }); @@ -10660,41 +11198,7 @@ diff --git a/client.js b/client.js + if (column === "in-progress") progress.push({ ...card, status: "task", since: startedAtOf(task) }); + else if (column === "done" && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "cancelled" ? "cancelled" : "done" }); + } - } -- 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; ++ } + for (const graph of Array.isArray(workflows) ? workflows : []) { + if (graph === null || typeof graph !== "object") continue; + const seenRuns = /* @__PURE__ */ new Set(); @@ -10776,7 +11280,35 @@ diff --git a/client.js b/client.js + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 0; -+} + } +-function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { +- const roster = useRoster((value) => value); +- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); +- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); +- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); +- const bot = roster.byId[sessionId]; +- const room2 = roster.roomsById[sessionId]; +- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); +- const streaming = entries2.some((entry) => entry.text); +- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); +- const relaying = tools.some((entry) => entry.relay); +- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); +- const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); +- const inputAt = Math.max(inputRef.current, pendingInput); +- inputRef.current = inputAt; +- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; +- const [shown, setShown] = (0, import_react7.useState)(null); +- const wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); +- if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { +- if (wantKey === null || shown?.key === wantKey) return void 0; +- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); +- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; +- if (wait <= 0) { +- make(); +- return void 0; +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -10822,80 +11354,15 @@ diff --git a/client.js b/client.js + if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; + event.preventDefault(); + send(); - } -- }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) ++ } + }) : null, + slot.open ? (0, import_react15.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Send it back")) : (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + slot.open = true; + redraw((count) => count + 1); + } }, t("Send back")), + (0, import_react15.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "accept") }, t("Accept")) - ); - } --function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- const deferred = source.kind === "bot" && source.deferred === true; -- const relayed = isRelayContext(data); -- const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { -- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -- }, [relayed, sessionId, turn, source.senderSessionId]); -- const order = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -- }); -- const peers = useTurnClock((state) => { -- if (!relayed) return ""; -- const turns = state[sessionId] ?? {}; -- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const hiddenTurns = roster.exchangeTurns[sessionId]; -- const unreadOpening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], order, hiddenTurns); -- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -- }); -- const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -- function triggerBody() { -- if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; -- if (source.role === "secret" || deferred) return null; -- if (source.role === "post") { -- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; -- const room2 = roster.roomsById[sessionId]; -- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -- } -- if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -- } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -- const run = relayed ? commRunOf( -- order.split(",").filter(Boolean).map(Number), -- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -- turn, -- hiddenTurns -- ) : null; -- if (run === "folded") return null; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -- const bots = [sender]; -- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -- } -- return (0, import_react6.createElement)(EventLine, { -- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -- bots, -- onOpen: (bot) => actions.openExchange(selfId, bot.id), -- whole: true -- }); ++ ); ++} +function AnswerFields({ card, busy, onAnswer }) { + const [, redraw] = (0, import_react15.useState)(0); + const question = card.kind === "question"; @@ -10913,9 +11380,18 @@ diff --git a/client.js b/client.js + onAnswer(card, { selected, custom }); + return; } -- if (source.kind === "schedule") { -- const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); +- const timer = setTimeout(make, wait); +- return () => clearTimeout(timer); +- }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); +- leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { +- if (visible) { +- setPhase("in"); +- return void 0; + if (custom === "") return; + onAnswer(card, custom); + }; @@ -10925,9 +11401,38 @@ diff --git a/client.js b/client.js + onAnswer(card, { selected: [label], custom: "" }); + return; } -- if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -- } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); +- return () => clearTimeout(timer); +- }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { +- if (phase !== null) return; +- endedAt.current = Date.now(); +- setShown(null); +- const before = shift.current; +- shift.current = null; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (before === null || !scroller || reducedMotion()) return; +- const delta = before - scroller.scrollTop; +- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); +- }, [phase]); +- (0, import_react7.useEffect)(() => { +- if (!running) actions.clearLive(sessionId); +- }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { +- if (phase !== "in") return void 0; +- const timer = setInterval(() => setNow(Date.now()), 1e4); +- return () => clearInterval(timer); +- }, [phase]); +- const host = useRunningHost(running); +- if (!host || phase === null || !shown) return null; +- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -10961,9 +11466,7 @@ diff --git a/client.js b/client.js + }), + (0, import_react15.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); - } --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 InboxRow({ wide, useRoster, useWork, useUi, actions }) { + const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; @@ -10987,30 +11490,11 @@ diff --git a/client.js b/client.js + wide ? (0, import_react15.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react15.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null + ); - } --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 Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; + return (0, import_react15.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react15.createElement)(Icon), (0, import_react15.createElement)("span", null, t(label))); - } --function Nothing() { -- return null; ++} +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"); @@ -11029,14 +11513,22 @@ diff --git a/client.js b/client.js + const writeBack = card.kind === "task" && card.task?.writeBack?.state === "failed" ? card.task.writeBack : void 0; + const unconfirmed = card.kind === "task" && card.task?.writeBack?.state === "unconfirmed" ? card.task.writeBack : void 0; + return (0, import_react15.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)( + { className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status, "data-run": card.kind === "workflow" ? card.run?.id : void 0 }, + (0, import_react15.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react15.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react15.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null), + (0, import_react15.createElement)( -+ "span", + "span", +- { className: "bt-typing-text" }, +- (0, import_react7.createElement)( + { className: "bt-inbox-copy" }, + (0, import_react15.createElement)( -+ "span", + "span", +- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null + { className: "bt-inbox-top" }, + (0, import_react15.createElement)(Status, { status: card.status }), + (0, import_react15.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta) @@ -11076,17 +11568,7 @@ diff --git a/client.js b/client.js + (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) + ); - } --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 useNow2(ms) { + const [now, setNow] = (0, import_react15.useState)(Date.now); + (0, import_react15.useEffect)(() => { @@ -11094,53 +11576,15 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [ms]); + return now; - } --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 openCard(actions, card) { + actions.closeInbox(); + if (card?.kind === "workflow") { + actions.openSettings("workflow"); + return; - } -- 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; ++ } + actions.openSession(card.chat); - } --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 InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { + const work = useWork((value) => value); + const sessions = useSessions((value) => value); @@ -11231,12 +11675,11 @@ diff --git a/client.js b/client.js + }, t(allDone ? "Show fewer" : "Show all")) : null + ); + }) -+ ) -+ ) - ); - } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); + ) + ) +- ), host); ++ ); ++} +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)} @@ -11306,10 +11749,15 @@ diff --git a/client.js b/client.js + title: String(task.title ?? "") + }; } --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/dsh-offer.js +-var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +-var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; +-var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); +-function showsDshRow(dsh, app) { +- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; +- if (dsh.kind === "npm") return true; +- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); +// src/client/board-card.js +var import_react16 = require("react"); +var TASK_SKIN = { @@ -11322,42 +11770,7 @@ 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, @@ -11406,8 +11819,16 @@ diff --git a/client.js b/client.js + footer, + actions + ); -+} -+ + } + +-// src/client/sidebar.js +-var import_react8 = require("react"); +-var import_react_dom3 = require("react-dom"); +-var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); +-function useMarkedUnread(id, active, marked, actions) { +- (0, import_react8.useEffect)(() => { +- if (active && marked) actions.clearUnread(id); +- }, [active, marked]); +// src/client/kanban.js +var import_react17 = require("react"); +var POLL_MS4 = 15e3; @@ -11419,7 +11840,36 @@ diff --git a/client.js b/client.js + (0, import_react17.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), + (0, import_react17.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) + ); -+} + } +-var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); +-var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; +-var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; +-var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { +- const [id, tool] = key.split("|"); +- return { id, tool: tool || void 0 }; +-}); +-var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); +-function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { +- const id = bot.id; +- const part = currentPart(bot); +- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); +- const running = useSessionStatus((map) => map.get(part)?.running === true); +- const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); +- const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; +- useMarkedUnread(id, active, marked, actions); +- const main = isMainOf(roster, id); +- const tile = roster.mainBotId === id; +- const approval = useApproval(useWork, (entry) => entry.botId === id); +- const alert = roster.questions?.[part] !== void 0 || approval; +- const doing = useActivity((map) => map[id]); +- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); +- const away = !running && groups.length > 0; +- const state = botState(running || away, doing, alert); +- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; +- if (bot.hidden && !active && !main) return null; +- const onContextMenu = (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); +function shortId(id) { + const hex = String(id ?? "").replace(/^task-/, ""); + return `#${hex.slice(-6)}`; @@ -11485,20 +11935,27 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); - } -- 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" }; -+ }; ++ } + }; +- const pinned = bot.pinned === true && !tile; +- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); +- if (tile && wide) { +- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-tile", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: preview || bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu +- }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), +- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), +- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null +- )); + walk("", 0); + const groups = (roster?.rooms ?? []).filter((room2) => room2 && room2.hidden !== true && room2.origin !== "face-to-face" && adminOf(roster, room2)).map((room2) => ({ kind: "group", id: room2.id, name: room2.displayName || room2.name || t("Group"), room: room2 })); + return { bots: rows, groups }; @@ -11537,16 +11994,11 @@ diff --git a/client.js b/client.js + return t("Cancelled"); + case "failed": + return t("Failed"); - 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) }; ++ default: + 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"); +- return (0, import_react8.createElement)( ++} +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -11615,9 +12067,16 @@ diff --git a/client.js b/client.js +} +function RailButton({ row, selected, meta, onSelect }) { + return (0, import_react17.createElement)( -+ "button", -+ { -+ type: "button", + "button", + { + type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: wide ? void 0 : bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu + className: "bt-board-rail-item", + "data-kind": row.kind, + "data-id": row.id, @@ -11625,11 +12084,47 @@ diff --git a/client.js b/client.js + "aria-current": selected ? "true" : void 0, + style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : void 0, + onClick: () => onSelect(row.kind === "group" ? { kind: "group", roomId: row.id } : { kind: "bot", botId: row.id }) -+ }, + }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + (0, import_react17.createElement)("span", { className: "bt-board-rail-name" }, row.name), + meta ? (0, import_react17.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null -+ ); -+} + ); + } +-function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { +- const id = room2.id; +- const fallback = useSessions((list) => previewOf(list, id)); +- const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; +- const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); +- const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; +- useMarkedUnread(id, active, marked, actions); +- const approval = useApproval(useWork, (entry) => entry.roomId === id); +- const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); +- const first = roster.byId[members[0]?.id]; +- const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; +- if (room2.hidden && !active) return null; +- const pinned = room2.pinned === true; +- const label = [room2.name, pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", approval ? t("Waiting for your approval") : ""].filter(Boolean).join(", "); +- return (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-room-id": id, +- onClick: () => actions.openSession(id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); +- } +function BoardCard2({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { + const column = columnOf(task); + const priority = priorityOf(task); @@ -11646,7 +12141,10 @@ diff --git a/client.js b/client.js + "data-column": column, + "data-priority": priority, + "data-kind": overall ? "group" : void 0 -+ }, + }, +- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( + idText: shortId(task.id), + emptyTitle: t("Untitled task"), + topExtra: [ @@ -11660,7 +12158,13 @@ diff --git a/client.js b/client.js + question ? (0, import_react17.createElement)("span", { key: "wait", className: "bt-board-wait" }, question) : null + ], + footer: (0, import_react17.createElement)( -+ "span", + "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null +- ); + { className: "bt-board-foot" }, + who ? (0, import_react17.createElement)("span", { className: "bt-board-who" }, who) : null, + Number.isFinite(finished) && finished > 0 ? (0, import_react17.createElement)("span", { key: "finished", className: "bt-board-when" }, relativeShort(finished)) : null, @@ -11684,23 +12188,72 @@ diff --git a/client.js b/client.js + onDragStart: () => onDrag?.(task.id), + onDragEnd: () => onDrag?.(null) + }); -+} -+function cardProps(task, roster, onMove, onDrag, partOf2, locked) { -+ return { task, roster, movable: !locked && canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; -+} -+function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull, locked }) { -+ const tree = lanes ? columnTree(tasks) : null; -+ const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; -+ const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; -+ return (0, import_react17.createElement)( -+ "section", -+ { -+ className: "bt-board-column", -+ "data-column": column, -+ "aria-label": columnTitle(column), -+ onDragOver: (event) => { -+ if (locked || !dragging) return; -+ event.preventDefault(); + } +-function AgentSessionRow({ session, agent, actions, renaming, index }) { +- const input = (0, import_react8.useRef)(null); +- (0, import_react8.useLayoutEffect)(() => { +- if (!renaming) return; +- input.current?.focus(); +- input.current?.select(); +- }, [renaming]); +- if (renaming) { +- const commit = (keep) => { +- const value = input.current?.value ?? ""; +- if (!keep || value.trim() === "" || value.trim() === session.title) { +- actions.endAgentRename(); +- return; +- } +- void (async () => { +- try { +- await actions.renameAgentSession(agent.id, session.id, value); +- actions.endAgentRename(); +- } catch (error) { +- actions.reportError(error, "agent-rename"); +- } +- })(); +- }; +- return (0, import_react8.createElement)( +- "div", +- { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, +- (0, import_react8.createElement)("input", { +- ref: input, +- defaultValue: session.title, +- "aria-label": t("Rename"), +- onKeyDown: (event) => { +- if (event.nativeEvent.isComposing) return; +- if (event.key === "Enter") commit(true); +- else if (event.key === "Escape") commit(false); +- }, +- onBlur: () => commit(true) +- }) +- ); +- } +- return (0, import_react8.createElement)( +- "button", ++function cardProps(task, roster, onMove, onDrag, partOf2, locked) { ++ return { task, roster, movable: !locked && canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; ++} ++function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull, locked }) { ++ const tree = lanes ? columnTree(tasks) : null; ++ const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; ++ const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; ++ return (0, import_react17.createElement)( ++ "section", + { +- type: "button", +- className: "bt-agent-sub-row", +- "aria-current": session.active ? "page" : void 0, +- style: { "--i": index }, +- onClick: () => actions.openAgentSession(session.id), +- onContextMenu: (event) => { ++ className: "bt-board-column", ++ "data-column": column, ++ "aria-label": columnTitle(column), ++ onDragOver: (event) => { ++ if (locked || !dragging) return; + event.preventDefault(); +- event.stopPropagation(); +- actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); + }, + onDrop: (event) => { + if (locked) return; @@ -11708,8 +12261,10 @@ diff --git a/client.js b/client.js + const id = event.dataTransfer?.getData("text/plain"); + if (id) onMove?.(id, column); + onDrag?.(null); -+ } -+ }, + } + }, +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") + (0, import_react17.createElement)( + "h3", + { className: "bt-board-column-head" }, @@ -11737,15 +12292,72 @@ diff --git a/client.js b/client.js + (0, import_react17.createElement)("span", { className: "bt-board-lane-name" }, lane.name), + lane.tasks.map((task) => (0, import_react17.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) + )) : plain.map((task) => (0, import_react17.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) -+ ); -+} + ); + } +-function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { +- const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { +- const row = list.byId[id]; +- return { +- id, +- // A blank Session's title is the workspace folder name; show the label instead. +- title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", +- at: row?.updatedAt ?? 0, +- running: row?.running === true, +- active: (row?.retainedBy?.mainView ?? 0) > 0 +- }; +- }).sort((a, b) => b.at - a.at))); +- const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ +- id, +- running: map.get(id)?.running === true, +- unread: map.get(id)?.completionUnread === true +- })))); +- const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); +- const sessions = JSON.parse(info).map((session) => ({ +- ...session, +- running: statuses[session.id]?.running ?? session.running, +- unread: statuses[session.id]?.unread ?? false, +- title: session.title === "" ? t("New session") : session.title +- })); +- const renaming = useUi((value) => value.renameAgent); +- const active = sessions.some((session) => session.active); +- const running = sessions.some((session) => session.running); +- const unread = sessions.some((session) => session.unread) || marked && !active; +- useMarkedUnread(agent.id, active, marked, actions); +- const preview = sessions[0]?.title ?? t("New session"); +- const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); +- const open = active && wide; +- return (0, import_react8.createElement)( +function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate, archiveFull, locked }) { + const columns = boardColumns(tasks); + const lanes = scope?.kind === "group"; + const selectedBot = scope?.kind === "bot" ? scope.botId : ""; + const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; + return (0, import_react17.createElement)( -+ "div", + "div", +- { className: "bt-agent" }, +- (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-agent-id": agent.id, +- title: wide ? void 0 : "DSH Agent", +- onClick: () => actions.openAgent(agent.id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); +- } +- }, +- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), +- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + { className: "bt-board-body" }, + (0, import_react17.createElement)( + "aside", @@ -11765,9 +12377,29 @@ diff --git a/client.js b/client.js + }), + rail.groups.length > 0 ? (0, import_react17.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, + rail.groups.map((row) => (0, import_react17.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) -+ ), + ), +- (0, import_react8.createElement)( + (0, import_react17.createElement)( -+ "div", + "div", +- { className: "bt-agent-sub", "data-open": open || void 0 }, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-agent-sub-in" }, +- (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { +- void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); +- } }, +- (0, import_react8.createElement)(PlusIcon), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) +- ), +- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { +- key: session.id, +- session, +- agent, +- actions, +- index: index + 1, +- renaming: renaming === session.id + { className: "bt-board-main" }, + error ? (0, import_react17.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, + lanes ? (0, import_react17.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, @@ -11797,11 +12429,30 @@ diff --git a/client.js b/client.js + onDrag, + archiveFull, + locked -+ })) -+ ) -+ ) -+ ); -+} + })) + ) + ) + ); + } +-var GLIDE = springEasing(1e3, 63); +-function useListGlide(listRef, layout, key) { +- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); +- (0, import_react8.useLayoutEffect)(() => { +- const list = listRef.current; +- if (!list) return; +- const next = /* @__PURE__ */ new Map(); +- const still = reducedMotion() || tops.current.layout !== layout; +- for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { +- const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; +- const top = row.offsetTop; +- next.set(id, top); +- const before = tops.current.map.get(id); +- if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { +- row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); +- } +- } +- tops.current = { layout, map: next }; +- }, [layout, key]); +function KanbanRow({ wide, useUi, actions }) { + const open = useUi ? useUi((value) => Boolean(value.board)) : false; + const label = t("Tasks"); @@ -11820,44 +12471,41 @@ diff --git a/client.js b/client.js + wide ? (0, import_react17.createElement)("span", { className: "bt-board-label" }, label) : null + ); } --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 TeamSidebar(props) { +- const { wide, useRoster, useUi, useSessions, actions } = props; +- const listRef = (0, import_react8.useRef)(null); +- const roster = useRoster((value) => value); +- const markedUnread = useUi((value) => value.unread); +- props.useRegistry((value) => value); +- const updated = useSessions((list) => { +- const stamps = {}; +- for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; +- return JSON.stringify(stamps); +- }); +- const order = (0, import_react8.useMemo)(() => { +- const stamps = JSON.parse(updated); +- const entries2 = [ +- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), +- ...roster.rooms.map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), +- // The Agent sorts by its most recent Session, falling back to when it was added. +- ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) +- ]; +- return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || b.at - a.at); +- }, [roster, updated]); +- const main = roster.byId[roster.mainBotId]; +- useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); +- useLandOnMainBot(roster, useSessions, actions); +- const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; +- const typeToSearch = (event) => { +- if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; +- if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; +- event.preventDefault(); +- actions.openPalette(event.key); +function scopeName(roster, scope) { + if (scope?.kind === "group") return roster.roomsById?.[scope.roomId]?.name || t("Group"); + if (scope?.kind === "bot") return roster.byId?.[scope.botId]?.name || ""; + return ""; - } --function 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) { ++} +function KanbanDialog({ roster, actions, leaving = false }) { + const locked = typeof roster?.readOnly === "string" && roster.readOnly.trim() !== ""; + const rail = boardRail(roster); @@ -11892,21 +12540,13 @@ 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 { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; ++ try { + replace(await actions.taskUpdate({ id, column })); + } catch (failure) { + setError(failure?.message ?? String(failure)); - } -- } -- 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)( -+ }; ++ } + }; +- return (0, import_react8.createElement)( + const create = async (title) => { + if (locked) return; + const text2 = String(title ?? "").trim(); @@ -11922,72 +12562,32 @@ diff --git a/client.js b/client.js + const name = scopeName(roster, scope); + return (0, import_react17.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)( +- { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, +- wide ? (0, import_react8.createElement)( +- "div", +- { className: "bt-side-top" }, +- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) +- ) : (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) +- ), +- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, +- (0, import_react8.createElement)( + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react17.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react17.createElement)( -+ "div", + "div", +- { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, +- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), +- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react17.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_react17.createElement)("h2", { id: "bt-board-heading" }, t("Board")), + (0, import_react17.createElement)("span", { className: "bt-mem-status" }, name), @@ -12013,23 +12613,18 @@ diff --git a/client.js b/client.js + onDrag: setDragging, + onCreate: create + }) -+ ) + ) ); } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; -- const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); -- }; -- visit(root); -- return calls; +-function UpdateButton({ useUpdate, actions }) { +- const update = useUpdate((value) => value); +- const [open, setOpen] = (0, import_react8.useState)(false); +- const [box, setBox] = (0, import_react8.useState)(null); +- const [error, setError] = (0, import_react8.useState)(""); +- const [dshError, setDshError] = (0, import_react8.useState)(""); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!open) return void 0; +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -12092,15 +12687,7 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} - } --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/role-labels.js @@ -12108,82 +12695,7 @@ diff --git a/client.js b/client.js +function roleText(role) { + if (role === null || role === void 0 || role === "") return ""; + return t(ROLE_LABELS[role] ?? String(role)); - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -- } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -- } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; -- } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); -- } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -- } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; ++} + +// src/client/projects-nav.js +var import_react18 = require("react"); @@ -12220,40 +12732,14 @@ 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 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_react18.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react18.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react18.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react18.createElement)( + "button", @@ -12279,50 +12765,14 @@ diff --git a/client.js b/client.js + rows, + note === "" || note === void 0 ? null : (0, import_react18.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_react18.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_react18.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_react18.createElement)( + "svg", @@ -12334,77 +12784,34 @@ diff --git a/client.js b/client.js +function ProjectRoleMenu({ project, x, y, onPick, onClose }) { + const ref = (0, import_react18.useRef)(null); + (0, import_react18.useEffect)(() => { -+ const onDown = (event) => { + const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; +- setOpen(false); + if (!ref.current?.contains(event.target)) onClose(); }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; -- }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); + const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); - } -+ const onKey = (event) => { + if (event.key === "Escape") onClose(); }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(scan); -- }); -- scan(); +- window.addEventListener("mousedown", onDown); + window.addEventListener("mousedown", onDown, true); -+ window.addEventListener("keydown", onKey, true); + window.addEventListener("keydown", onKey, true); return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); +- window.removeEventListener("mousedown", onDown); + window.removeEventListener("mousedown", onDown, true); -+ window.removeEventListener("keydown", onKey, true); + window.removeEventListener("keydown", onKey, true); }; -- }, []); --} --function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -- const roster = useRoster((value) => value); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -- const streaming = entries2.some((entry) => entry.text); -- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -- const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); -- const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); -- const inputAt = Math.max(inputRef.current, pendingInput); -- inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); -- const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); -- if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { -- if (wantKey === null || shown?.key === wantKey) return void 0; -- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); -- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -- if (wait <= 0) { -- make(); -- return void 0; +- }, [open]); +- const phase = update?.phase ?? "idle"; +- const dsh = update?.dsh; +- const dshPhase = dsh?.phase ?? "idle"; +- const dshRow = showsDshRow(dsh, update?.app); +- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; +- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react18.createElement)( @@ -12438,19 +12845,7 @@ 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_react18.useState)(false); @@ -12467,686 +12862,125 @@ 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_react18.useEffect)(() => { + if (!expanded) return; + void load(); + }, [expanded, load]); -+ const toggle = () => { -+ setNote(""); -+ if (!wide) { -+ void open(""); -+ return; -+ } -+ setExpanded((value) => !value); -+ }; -+ const listed = expanded && wide; -+ return (0, import_react18.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); -+ { className: "bt-projects" }, -+ (0, import_react18.createElement)( -+ "button", -+ { -+ type: "button", -+ className: "bt-row bt-projects-row", -+ "aria-expanded": listed ? "true" : "false", -+ "aria-label": t("Projects"), -+ title: wide ? void 0 : t("Projects"), -+ onClick: toggle -+ }, -+ (0, import_react18.createElement)("span", { className: "bt-projects-mark" }, (0, import_react18.createElement)(ProjectsMark)), -+ wide ? (0, import_react18.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null, -+ wide ? (0, import_react18.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react18.createElement)(Chevron)) : null -+ ), -+ listed ? (0, import_react18.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null, -+ menu === null ? null : (0, import_react18.createElement)(ProjectRoleMenu, { -+ project: menu.project, -+ x: menu.x, -+ y: menu.y, -+ onClose: () => setMenu(null), -+ onPick: async (role) => { -+ const project = menu.project; -+ setMenu(null); -+ try { -+ const answer = await read("project-role", { id: project.id, role }); -+ setNote(roleNote(answer)); -+ if (answer?.changed === true) void load(); -+ } catch (failure) { -+ setNote(failure?.message ?? String(failure)); -+ } -+ } -+ }) -+ ); - } - - // src/client/sidebar.js --var import_react8 = require("react"); --var import_react_dom3 = require("react-dom"); -+var import_react19 = require("react"); -+var import_react_dom4 = require("react-dom"); - var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); - function useMarkedUnread(id, active, marked, actions) { -- (0, import_react8.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { - if (active && marked) actions.clearUnread(id); - }, [active, marked]); - } --var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); -+var PinMark = () => (0, import_react19.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react19.createElement)(PinMarkIcon)); - var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; - var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; --var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { -+var workKeys = (text2) => text2 === "" ? [] : text2.split(",").map((key) => { - const [id, tool] = key.split("|"); - return { id, tool: tool || void 0 }; - }); --var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); --function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { -+var workLine = (lead, tool, more) => (0, import_react19.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); -+function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { - const id = bot.id; - const part = currentPart(bot); -- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); -+ const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part], unsentReplyTurns(bot, part), botNames(roster)), roster)); - const running = useSessionStatus((map) => map.get(part)?.running === true); - const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); - const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; - useMarkedUnread(id, active, marked, actions); - const main = isMainOf(roster, id); -- const tile = roster.mainBotId === id; -+ const tile = roster.mainBotId === id && !asRow; - const approval = useApproval(useWork, (entry) => entry.botId === id); -- const alert = roster.questions?.[part] !== void 0 || approval; -+ const waiting = approval || (bot.pendingCount ?? 0) > 0; -+ const alert = roster.questions?.[part] !== void 0 || waiting; - const doing = useActivity((map) => map[id]); - const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); - const away = !running && groups.length > 0; - const state = botState(running || away, doing, alert); -- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; -- if (bot.hidden && !active && !main) return null; -+ const shownAway = away ? awayGroups(groups, roster.roomsById) : []; -+ const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t("a group chat"), shownAway[0].tool, shownAway.length - 1) : null; -+ if (!isShownRemoteChat(bot) && !active && !main) return null; - const onContextMenu = (event) => { - event.preventDefault(); - actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); - }; - const pinned = bot.pinned === true && !tile; -- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); -+ const remote = Boolean(bot.remote); -+ const shown = remote ? friendBotTitle(bot) : bot.name; -+ const secondLine = awayLine || preview || (remote ? bot.remote?.gone ? t("no longer shared") : t("Friend's Bot") : ""); -+ const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); - if (tile && wide) { -- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( -+ return (0, import_react19.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react19.createElement)( - "button", - { - type: "button", -@@ -4785,55 +4785,131 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio - onClick: () => actions.openSession(id), - onContextMenu - }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), -- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), -- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null -+ (0, import_react19.createElement)(BotAvatar, { bot, size: 60, state, main: true }), -+ (0, import_react19.createElement)("span", { className: "bt-tile-name" }, shown), -+ // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. -+ unread && !waiting && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, -+ bot.role ? (0, import_react19.createElement)("span", { className: "bt-chip" }, bot.role) : null - )); - } -- return (0, import_react8.createElement)( -+ const unreadMark = wide && waiting && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; -+ const row = (0, import_react19.createElement)( - "button", - { - type: "button", - className: "bt-row", - "aria-label": label, - "aria-current": active ? "page" : void 0, -+ // An own Bot's hover text names its model (#366); a friend's Bot runs on its owner's model. - "data-bot-id": id, -- title: wide ? void 0 : bot.name, -+ title: remote ? shown : botRowTitle(shown, modelOf(bot, roster)?.name, wide), - onClick: () => actions.openSession(id), - onContextMenu - }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( -+ (0, import_react19.createElement)(BotAvatar, { bot, size: 36, state, main }), -+ pinned && !wide ? (0, import_react19.createElement)(PinMark) : null, -+ wide ? (0, import_react19.createElement)( - "span", - { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react19.createElement)( -+ "span", -+ { className: "bt-row-name" }, -+ (0, import_react19.createElement)("span", { className: "bt-row-title" }, shown), -+ bot.remote ? (0, import_react19.createElement)(RemoteMark, { remote: bot.remote }) : null, -+ pinned ? (0, import_react19.createElement)(PinMark) : null -+ ), -+ secondLine ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, secondLine) : null - ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ unreadMark -+ ); -+ const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); -+ const settings = wide ? (0, import_react19.createElement)("button", { -+ type: "button", -+ className: "bt-row-settings", -+ "aria-label": settingsLabel, -+ title: settingsLabel, -+ onFocus: (event) => keepSidebarStill(event.currentTarget), -+ onClick: (event) => { -+ event.stopPropagation(); -+ keepSidebarStill(event.currentTarget); -+ actions.openSettings("bots", id); -+ } -+ }, (0, import_react19.createElement)(GearIcon)) : null; -+ if (settings) return (0, import_react19.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); -+ return row; -+} -+function keepSidebarStill(node) { -+ const reset = () => { -+ for (let at = node?.parentElement; at; at = at.parentElement) if (at.scrollLeft > 0) at.scrollLeft = 0; -+ const page = typeof document === "undefined" ? null : document.scrollingElement; -+ if (page && page.scrollLeft > 0) page.scrollLeft = 0; -+ }; -+ reset(); -+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); -+} -+function RemoteMark({ remote }) { -+ return (0, import_react19.createElement)( -+ "span", -+ { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, -+ (0, import_react19.createElement)( -+ "svg", -+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, -+ (0, import_react19.createElement)("circle", { cx: 12, cy: 12, r: 9 }), -+ (0, import_react19.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) -+ ) -+ ); -+} -+function isPastFaceRoom(room2) { -+ if (room2?.origin !== "face-to-face") return false; -+ const status = room2.face?.status; -+ if (status === "closed" || status === "left") return true; -+ const expires = room2.face?.expiresAt; -+ return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); -+} -+function FaceMark() { -+ return (0, import_react19.createElement)( -+ "span", -+ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, -+ (0, import_react19.createElement)( -+ "svg", -+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, -+ (0, import_react19.createElement)("circle", { cx: 9, cy: 8, r: 3 }), -+ (0, import_react19.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), -+ (0, import_react19.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) -+ ) - ); - } - function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { - const id = room2.id; -- const fallback = useSessions((list) => previewOf(list, id)); -- const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; -+ const fallback = useSessions((list) => nameMentions(previewOf(list, id, [], [], botNames(roster)), roster)); -+ const preview = nameMentions(visibleText(room2.preview), roster) || fallback; - const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); - const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; - useMarkedUnread(id, active, marked, actions); - const approval = useApproval(useWork, (entry) => entry.roomId === id); -+ const question = roster.questions?.[id] !== void 0; - const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); - const first = roster.byId[members[0]?.id]; - const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; - if (room2.hidden && !active) return null; - const pinned = room2.pinned === true; -- const label = [room2.name, pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", approval ? t("Waiting for your approval") : ""].filter(Boolean).join(", "); -- return (0, import_react8.createElement)( -+ const title = roomTitle(room2); -+ const from = room2.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : ""; -+ const hover = from && from !== title ? `${title} · ${from}` : title; -+ const waiting = approval || (room2.pendingCount ?? 0) > 0; -+ const alert = question || waiting; -+ const face = room2.origin === "face-to-face"; -+ const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", "); -+ return (0, import_react19.createElement)( - "button", - { - type: "button", - className: "bt-row", - "aria-label": label, - "aria-current": active ? "page" : void 0, -+ // The full title on hover. The row wraps the number-first form so the second -+ // name is not cut to "SelfB…" (#1102, #304, #458). -+ title: hover, - "data-room-id": id, - onClick: () => actions.openSession(id), - onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, - actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); - } - }, -- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( -+ (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), -+ pinned && !wide ? (0, import_react19.createElement)(PinMark) : null, -+ wide ? (0, import_react19.createElement)( - "span", - { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react19.createElement)("span", { className: "bt-row-name" }, (0, import_react19.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react19.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react19.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react19.createElement)(PinMark) : null), -+ workingLine ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, preview) : null - ) : null, -- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ wide && alert && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null - ); - } - function AgentSessionRow({ session, agent, actions, renaming, index }) { -- const input = (0, import_react8.useRef)(null); -- (0, import_react8.useLayoutEffect)(() => { -+ const input = (0, import_react19.useRef)(null); -+ (0, import_react19.useLayoutEffect)(() => { - if (!renaming) return; - input.current?.focus(); - input.current?.select(); -@@ -4875,10 +4875,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { - } - })(); - }; -- return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "div", - { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, -- (0, import_react8.createElement)("input", { -+ (0, import_react19.createElement)("input", { - ref: input, - defaultValue: session.title, - "aria-label": t("Rename"), -@@ -4891,7 +4891,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { - }) - ); - } -- return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "button", - { - type: "button", -@@ -4905,8 +4905,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { - actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); - } - }, -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") -+ (0, import_react19.createElement)("span", { className: "bt-agent-sub-title" }, session.title), -+ (0, import_react19.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") - ); - } - function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, - const preview = sessions[0]?.title ?? t("New session"); - const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); - const open = active && wide; -- return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "div", - { className: "bt-agent" }, -- (0, import_react8.createElement)( -+ (0, import_react19.createElement)( - "button", - { - type: "button", -@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, - actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); - } - }, -- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), -- wide ? (0, import_react8.createElement)( -+ (0, import_react19.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react19.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), -+ wide ? (0, import_react19.createElement)( - "span", - { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), -- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react19.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), -+ preview ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, preview) : null - ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null - ), -- (0, import_react8.createElement)( -+ (0, import_react19.createElement)( - "div", - { className: "bt-agent-sub", "data-open": open || void 0 }, -- (0, import_react8.createElement)( -+ (0, import_react19.createElement)( - "div", - { className: "bt-agent-sub-in" }, -- (0, import_react8.createElement)( -+ (0, import_react19.createElement)( - "button", - { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { - void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); - } }, -- (0, import_react8.createElement)(PlusIcon), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) -+ (0, import_react19.createElement)(PlusIcon), -+ (0, import_react19.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) - ), -- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { -+ sessions.map((session, index) => (0, import_react19.createElement)(AgentSessionRow, { - key: session.id, - session, - agent, -@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, - } - var GLIDE = springEasing(1e3, 63); - function useListGlide(listRef, layout, key) { -- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); -- (0, import_react8.useLayoutEffect)(() => { -+ const tops = (0, import_react19.useRef)({ layout, map: /* @__PURE__ */ new Map() }); -+ (0, import_react19.useLayoutEffect)(() => { - const list = listRef.current; - if (!list) return; - const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +5014,44 @@ function useListGlide(listRef, layout, key) { - tops.current = { layout, map: next }; - }, [layout, key]); - } -+var NodeActions = class extends import_react19.Component { -+ constructor(props) { -+ super(props); -+ this.state = { failed: false }; -+ } -+ static getDerivedStateFromError() { -+ return { failed: true }; -+ } -+ componentDidCatch(error) { -+ console.error("flybot: nodeActions failed", error); -+ } -+ render() { -+ return this.state.failed ? null : (0, import_react19.createElement)(this.props.component, { bot: this.props.bot }); -+ } -+}; -+function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { -+ const { bot, depth, children } = node; -+ const closed = collapsed[bot.id] === true; -+ return (0, import_react19.createElement)( -+ import_react19.Fragment, -+ null, -+ (0, import_react19.createElement)( -+ "div", -+ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, -+ children.length ? (0, import_react19.createElement)( -+ "button", -+ { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, -+ (0, import_react19.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") -+ ) : (0, import_react19.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), -+ (0, import_react19.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), -+ typeof actions === "function" ? (0, import_react19.createElement)(NodeActions, { component: actions, bot }) : null -+ ), -+ closed ? null : children.map((child) => (0, import_react19.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) -+ ); -+} - function TeamSidebar(props) { - const { wide, useRoster, useUi, useSessions, actions } = props; -- const listRef = (0, import_react8.useRef)(null); -+ const listRef = (0, import_react19.useRef)(null); - const roster = useRoster((value) => value); - const markedUnread = useUi((value) => value.unread); - props.useRegistry((value) => value); -@@ -5025,17 +5025,34 @@ function TeamSidebar(props) { - for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; - return JSON.stringify(stamps); - }); -- const order = (0, import_react8.useMemo)(() => { -+ const order = (0, import_react19.useMemo)(() => { - const stamps = JSON.parse(updated); - const entries2 = [ -- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), -- ...roster.rooms.map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), -+ // Own Bots, friends' Bots, groups and face rooms share one list. A remote chat -+ // still uses BotRow's globe; gone / hidden / home-level remotes stay out. -+ ...roster.bots.filter((bot) => bot.id !== roster.mainBotId && isShownRemoteChat(bot)).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), -+ ...roster.rooms.filter((room2) => !isPastFaceRoom(room2)).map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), - // The Agent sorts by its most recent Session, falling back to when it was added. - ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) - ]; -- return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || b.at - a.at); -+ const foreign = (entry) => entry.kind === "bot" && entry.item.remote || entry.kind === "room" && (entry.item.origin === "face-to-face" || entry.item.sharedFrom); -+ return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || Number(foreign(a)) - Number(foreign(b)) || b.at - a.at); - }, [roster, updated]); - const main = roster.byId[roster.mainBotId]; -+ const readOnlySaid = typeof roster.readOnly === "string" && roster.readOnly.trim() !== ""; -+ const readOnlyReload = (0, import_react19.createElement)("button", { -+ type: "button", -+ className: "bt-read-only-reload", -+ onClick: () => { -+ void Promise.resolve(actions.reloadTeam?.()).catch((error) => actions.reportError(error, "reload")); -+ } -+ }, t("Reload")); -+ const structure = useSidebarStructure(); -+ const [collapsed, setCollapsed] = (0, import_react19.useState)({}); -+ const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); -+ const inTree = (entry) => entry.kind === "bot" && !entry.item.remote; -+ const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false; -+ const tree = structure && wide && main ? botTree([main, ...order.filter(listed).map((entry) => entry.item)], main.id, (id) => structure.parentOf(id)) : null; - 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,46 @@ function TeamSidebar(props) { - event.preventDefault(); - actions.openPalette(event.key); - }; -- return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "div", - { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, -- wide ? (0, import_react8.createElement)( -+ wide ? (0, import_react19.createElement)( - "div", - { className: "bt-side-top" }, -- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -- ) : (0, import_react8.createElement)( -- import_react8.Fragment, -+ (0, import_react19.createElement)("span", { className: "bt-brand" }, (0, import_react19.createElement)(BrandMark, { size: 22 }), (0, import_react19.createElement)(BrandName)), -+ (0, import_react19.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react19.createElement)(SearchIcon)), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react19.createElement)(PlusIcon)) -+ ) : (0, import_react19.createElement)( -+ import_react19.Fragment, - null, -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -+ (0, import_react19.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -+ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react19.createElement)(PlusIcon)) - ), -- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, -- (0, import_react8.createElement)( -+ wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, -+ wide && readOnlySaid ? readOnlyReload : null, -+ !wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, -+ !wide && readOnlySaid ? readOnlyReload : null, -+ (0, import_react19.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), -+ (0, import_react19.createElement)(KanbanRow, { wide, useUi, actions }), -+ (0, import_react19.createElement)(ProjectsNav, { wide }), -+ (0, import_react19.createElement)( - "div", - { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, -- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), -- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) -+ tree ? (0, import_react19.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react19.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react19.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react19.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), -+ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react19.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react19.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react19.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), -+ wide ? (0, import_react19.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react19.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null - ) - ); - } - function UpdateButton({ useUpdate, actions }) { - const update = useUpdate((value) => value); -- const [open, setOpen] = (0, import_react8.useState)(false); -- const [box, setBox] = (0, import_react8.useState)(null); -- const [error, setError] = (0, import_react8.useState)(""); -- const [dshError, setDshError] = (0, import_react8.useState)(""); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -+ const [open, setOpen] = (0, import_react19.useState)(false); -+ const [box, setBox] = (0, import_react19.useState)(null); -+ const [error, setError] = (0, import_react19.useState)(""); -+ const [dshError, setDshError] = (0, import_react19.useState)(""); -+ const button = (0, import_react19.useRef)(null); -+ (0, import_react19.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 toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); +- setOpen((value) => !value); +- }; +- const start = () => { +- setError(""); +- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); +- }; +- const startDsh = () => { +- setDshError(""); +- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); ++ setNote(""); ++ if (!wide) { ++ void open(""); ++ return; ++ } ++ setExpanded((value) => !value); }; - const 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 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 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 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_react19.createElement)( -+ import_react19.Fragment, - null, +- null, - (0, import_react8.createElement)("button", { -+ (0, import_react19.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 +- 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_react19.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react19.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -+ open && box ? (0, import_react_dom4.createPortal)((0, import_react19.createElement)( - "div", +- "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, -+ { 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_react19.createElement)( -+ import_react19.Fragment, - null, +- 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)( -+ (0, import_react19.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), -+ (0, import_react19.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_react19.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -+ (0, import_react19.createElement)( - "div", - { className: "bt-update-actions" }, +- "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", { -+ update.notesUrl ? (0, import_react19.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -+ phase === "installed" ? null : (0, import_react19.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, +- 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)( -+ dshRow ? (0, import_react19.createElement)( - "div", - { className: "bt-update-dsh" }, +- "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)( -+ (0, import_react19.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -+ (0, import_react19.createElement)("div", { className: "bt-update-text" }, dshText), -+ dshMessage ? (0, import_react19.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -+ (0, import_react19.createElement)( - "div", - { className: "bt-update-actions" }, +- "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", { -+ dsh.kind === "app" ? dsh.available ? (0, import_react19.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_react19.createElement)("button", { - type: "button", - className: "bt-update-go", - disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { - ), document.body) : null +- type: "button", +- className: "bt-update-go", +- disabled: dshPhase === "installing", +- onClick: startDsh +- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null +- ), document.body) : null ++ const listed = expanded && wide; ++ return (0, import_react18.createElement)( ++ "div", ++ { className: "bt-projects" }, ++ (0, import_react18.createElement)( ++ "button", ++ { ++ type: "button", ++ className: "bt-row bt-projects-row", ++ "aria-expanded": listed ? "true" : "false", ++ "aria-label": t("Projects"), ++ title: wide ? void 0 : t("Projects"), ++ onClick: toggle ++ }, ++ (0, import_react18.createElement)("span", { className: "bt-projects-mark" }, (0, import_react18.createElement)(ProjectsMark)), ++ wide ? (0, import_react18.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null, ++ wide ? (0, import_react18.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react18.createElement)(Chevron)) : null ++ ), ++ listed ? (0, import_react18.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null, ++ menu === null ? null : (0, import_react18.createElement)(ProjectRoleMenu, { ++ project: menu.project, ++ x: menu.x, ++ y: menu.y, ++ onClose: () => setMenu(null), ++ onPick: async (role) => { ++ const project = menu.project; ++ setMenu(null); ++ try { ++ const answer = await read("project-role", { id: project.id, role }); ++ setNote(roleNote(answer)); ++ if (answer?.changed === true) void load(); ++ } catch (failure) { ++ setNote(failure?.message ?? String(failure)); ++ } ++ } ++ }) ); } -function ConnectPlugins({ wide, actions }) { @@ -13156,8 +12990,7 @@ diff --git a/client.js b/client.js - const button = (0, import_react8.useRef)(null); - const [box, setBox] = (0, import_react8.useState)(null); - (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); -+function ConnectApps({ wide, actions }) { - if (!wide) return null; +- if (!wide) return null; - const show = () => { - clearTimeout(timer.current); - timer.current = setTimeout(() => { @@ -13173,15 +13006,62 @@ diff --git a/client.js b/client.js - setTip(false); - }; - return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "span", - { className: "bt-connect-wrap" }, +- "span", +- { className: "bt-connect-wrap" }, - (0, import_react8.createElement)( -+ (0, import_react19.createElement)( ++ ++// src/client/sidebar.js ++var import_react19 = require("react"); ++var import_react_dom4 = require("react-dom"); ++var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); ++function useMarkedUnread(id, active, marked, actions) { ++ (0, import_react19.useEffect)(() => { ++ if (active && marked) actions.clearUnread(id); ++ }, [active, marked]); ++} ++var PinMark = () => (0, import_react19.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react19.createElement)(PinMarkIcon)); ++var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; ++var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; ++var workKeys = (text2) => text2 === "" ? [] : text2.split(",").map((key) => { ++ const [id, tool] = key.split("|"); ++ return { id, tool: tool || void 0 }; ++}); ++var workLine = (lead, tool, more) => (0, import_react19.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); ++function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { ++ const id = bot.id; ++ const part = currentPart(bot); ++ const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part], unsentReplyTurns(bot, part), botNames(roster)), roster)); ++ const running = useSessionStatus((map) => map.get(part)?.running === true); ++ const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); ++ const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; ++ useMarkedUnread(id, active, marked, actions); ++ const main = isMainOf(roster, id); ++ const tile = roster.mainBotId === id && !asRow; ++ const approval = useApproval(useWork, (entry) => entry.botId === id); ++ const waiting = approval || (bot.pendingCount ?? 0) > 0; ++ const alert = roster.questions?.[part] !== void 0 || waiting; ++ const doing = useActivity((map) => map[id]); ++ const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); ++ const away = !running && groups.length > 0; ++ const state = botState(running || away, doing, alert); ++ const shownAway = away ? awayGroups(groups, roster.roomsById) : []; ++ const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t("a group chat"), shownAway[0].tool, shownAway.length - 1) : null; ++ if (!isShownRemoteChat(bot) && !active && !main) return null; ++ const onContextMenu = (event) => { ++ event.preventDefault(); ++ actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); ++ }; ++ const pinned = bot.pinned === true && !tile; ++ const remote = Boolean(bot.remote); ++ const shown = remote ? friendBotTitle(bot) : bot.name; ++ const secondLine = awayLine || preview || (remote ? bot.remote?.gone ? t("no longer shared") : t("Friend's Bot") : ""); ++ const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); ++ if (tile && wide) { ++ return (0, import_react19.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react19.createElement)( "button", -- { + { - ref: button, -- type: "button", + type: "button", - className: "bt-connect bt-connect-foot", - "aria-describedby": tip ? tipId : void 0, - onMouseEnter: show, @@ -13189,234 +13069,564 @@ diff --git a/client.js b/client.js - onFocus: show, - onBlur: hide, - onClick: () => actions.selectPanel("plugins") -- }, ++ className: "bt-tile", ++ "aria-label": label, ++ "aria-current": active ? "page" : void 0, ++ "data-bot-id": id, ++ title: preview || bot.name, ++ onClick: () => actions.openSession(id), ++ onContextMenu + }, - (0, import_react8.createElement)(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", ++ (0, import_react19.createElement)(BotAvatar, { bot, size: 60, state, main: true }), ++ (0, import_react19.createElement)("span", { className: "bt-tile-name" }, shown), ++ // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. ++ unread && !waiting && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, ++ bot.role ? (0, import_react19.createElement)("span", { className: "bt-chip" }, bot.role) : null ++ )); ++ } ++ const unreadMark = wide && waiting && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; ++ const row = (0, import_react19.createElement)( ++ "button", ++ { ++ type: "button", ++ className: "bt-row", ++ "aria-label": label, ++ "aria-current": active ? "page" : void 0, ++ // An own Bot's hover text names its model (#366); a friend's Bot runs on its owner's model. ++ "data-bot-id": id, ++ title: remote ? shown : botRowTitle(shown, modelOf(bot, roster)?.name, wide), ++ onClick: () => actions.openSession(id), ++ onContextMenu ++ }, ++ (0, import_react19.createElement)(BotAvatar, { bot, size: 36, state, main }), ++ pinned && !wide ? (0, import_react19.createElement)(PinMark) : null, ++ wide ? (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 -+ { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, -+ (0, import_react19.createElement)(PlugIcon), -+ t("Connect apps") -+ ) ++ { className: "bt-row-body" }, ++ (0, import_react19.createElement)( ++ "span", ++ { className: "bt-row-name" }, ++ (0, import_react19.createElement)("span", { className: "bt-row-title" }, shown), ++ bot.remote ? (0, import_react19.createElement)(RemoteMark, { remote: bot.remote }) : null, ++ pinned ? (0, import_react19.createElement)(PinMark) : null ++ ), ++ secondLine ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ++ ) : null, ++ unreadMark ); ++ const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); ++ const settings = wide ? (0, import_react19.createElement)("button", { ++ type: "button", ++ className: "bt-row-settings", ++ "aria-label": settingsLabel, ++ title: settingsLabel, ++ onFocus: (event) => keepSidebarStill(event.currentTarget), ++ onClick: (event) => { ++ event.stopPropagation(); ++ keepSidebarStill(event.currentTarget); ++ actions.openSettings("bots", id); ++ } ++ }, (0, import_react19.createElement)(GearIcon)) : null; ++ if (settings) return (0, import_react19.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); ++ return row; ++} ++function keepSidebarStill(node) { ++ const reset = () => { ++ for (let at = node?.parentElement; at; at = at.parentElement) if (at.scrollLeft > 0) at.scrollLeft = 0; ++ const page = typeof document === "undefined" ? null : document.scrollingElement; ++ if (page && page.scrollLeft > 0) page.scrollLeft = 0; ++ }; ++ reset(); ++ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); } - function YouAvatar() { +-function YouAvatar() { - return (0, import_react8.createElement)( ++function RemoteMark({ remote }) { + return (0, import_react19.createElement)( "span", - { className: "bt-you-wrap" }, +- { 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")) -+ (0, import_react19.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react19.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -+ (0, import_react19.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, ++ (0, import_react19.createElement)( ++ "svg", ++ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, ++ (0, import_react19.createElement)("circle", { cx: 12, cy: 12, r: 9 }), ++ (0, import_react19.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) ++ ) ); } - var openShellSettings = null; -@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { - else openHarnessSettings(); - }); +-var openShellSettings = null; +-var openShellOnboarding = null; +-var shellShortcuts = null; +-function holdShellShortcuts(shortcuts) { +- shellShortcuts = shortcuts; +- return () => { +- if (shellShortcuts === shortcuts) shellShortcuts = null; +- }; +-} +-function openHarnessSettings() { +- requestAnimationFrame(() => { +- if (openShellSettings) { +- openShellSettings(); +- return; +- } +- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); +- if (desktop) openThroughMenu(desktop); +- else pressSettingsShortcut(); +- }); +-} +-function openHarnessOnboarding(id) { +- requestAnimationFrame(() => { +- if (openShellOnboarding) openShellOnboarding(id); +- else openHarnessSettings(); +- }); ++function isPastFaceRoom(room2) { ++ if (room2?.origin !== "face-to-face") return false; ++ const status = room2.face?.status; ++ if (status === "closed" || status === "left") return true; ++ const expires = room2.face?.expiresAt; ++ return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); } -+var MODELS_STEP_ID = "flybot-models"; -+var MODELS_STEP_ORDER = 1e3; -+function ModelsSettingsStep({ explicit, complete, openSection }) { -+ (0, import_react19.useLayoutEffect)(() => { -+ if (explicit) openSection("models"); -+ complete(); -+ }, []); -+ return null; -+} -+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 openThroughMenu(trigger) { +- const before = new Set(document.querySelectorAll('[role="menu"]')); +- trigger.click(); +- let frames = 0; +- const look = () => { +- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); +- const item = menu?.querySelector('button[role="menuitem"]'); +- if (item) item.click(); +- else if (++frames < 30) requestAnimationFrame(look); +- else pressSettingsShortcut(); +- }; +- requestAnimationFrame(look); ++function FaceMark() { ++ return (0, import_react19.createElement)( ++ "span", ++ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, ++ (0, import_react19.createElement)( ++ "svg", ++ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, ++ (0, import_react19.createElement)("circle", { cx: 9, cy: 8, r: 3 }), ++ (0, import_react19.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), ++ (0, import_react19.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) ++ ) ++ ); + } +-function pressSettingsShortcut() { +- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; +- if (!binding) return; +- const held = new Set(binding.modifiers); +- window.dispatchEvent(new KeyboardEvent("keydown", { +- code: binding.code, +- key: binding.code === "Comma" ? "," : "", +- bubbles: true, +- cancelable: true, +- ctrlKey: held.has("control"), +- altKey: held.has("alt"), +- shiftKey: held.has("shift"), +- metaKey: held.has("meta") +- })); ++function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { ++ const id = room2.id; ++ const fallback = useSessions((list) => nameMentions(previewOf(list, id, [], [], botNames(roster)), roster)); ++ const preview = nameMentions(visibleText(room2.preview), roster) || fallback; ++ const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); ++ const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; ++ useMarkedUnread(id, active, marked, actions); ++ const approval = useApproval(useWork, (entry) => entry.roomId === id); ++ const question = roster.questions?.[id] !== void 0; ++ const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); ++ const first = roster.byId[members[0]?.id]; ++ const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; ++ if (room2.hidden && !active) return null; ++ const pinned = room2.pinned === true; ++ const title = roomTitle(room2); ++ const from = room2.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : ""; ++ const hover = from && from !== title ? `${title} · ${from}` : title; ++ const waiting = approval || (room2.pendingCount ?? 0) > 0; ++ const alert = question || waiting; ++ const face = room2.origin === "face-to-face"; ++ const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", "); ++ return (0, import_react19.createElement)( ++ "button", ++ { ++ type: "button", ++ className: "bt-row", ++ "aria-label": label, ++ "aria-current": active ? "page" : void 0, ++ // The full title on hover. The row wraps the number-first form so the second ++ // name is not cut to "SelfB…" (#1102, #304, #458). ++ title: hover, ++ "data-room-id": id, ++ onClick: () => actions.openSession(id), ++ onContextMenu: (event) => { ++ event.preventDefault(); ++ actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); ++ } ++ }, ++ (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), ++ pinned && !wide ? (0, import_react19.createElement)(PinMark) : null, ++ wide ? (0, import_react19.createElement)( ++ "span", ++ { className: "bt-row-body" }, ++ (0, import_react19.createElement)("span", { className: "bt-row-name" }, (0, import_react19.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react19.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react19.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react19.createElement)(PinMark) : null), ++ workingLine ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, preview) : null ++ ) : null, ++ wide && alert && !active ? (0, import_react19.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ ); } - function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { +-function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { - const [open, setOpen] = (0, import_react8.useState)(false); - (0, import_react8.useLayoutEffect)(() => { -+ const [open, setOpen] = (0, import_react19.useState)(false); -+ (0, import_react19.useLayoutEffect)(() => { - if (!seat) return void 0; - actions.seatAccount(true); - return () => actions.seatAccount(false); - }, [seat]); +- 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)(() => { -+ const [box, setBox] = (0, import_react19.useState)(null); -+ const button = (0, import_react19.useRef)(null); -+ (0, import_react19.useEffect)(() => { - if (settingsOpen) setOpen(false); - }, [settingsOpen]); +- if (settingsOpen) setOpen(false); +- }, [settingsOpen]); - (0, import_react8.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { - if (!openSettings) return void 0; - openShellSettings = openSettings; - return () => { - if (openShellSettings === openSettings) openShellSettings = null; - }; - }, [openSettings]); +- if (!openSettings) return void 0; +- openShellSettings = openSettings; +- return () => { +- if (openShellSettings === openSettings) openShellSettings = null; +- }; +- }, [openSettings]); - (0, import_react8.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { - if (!openOnboarding) return void 0; - openShellOnboarding = openOnboarding; - return () => { - if (openShellOnboarding === openOnboarding) openShellOnboarding = null; - }; - }, [openOnboarding]); +- if (!openOnboarding) return void 0; +- openShellOnboarding = openOnboarding; +- return () => { +- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; +- }; +- }, [openOnboarding]); - (0, import_react8.useEffect)(() => { -+ (0, import_react19.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")], +- if (!open) return void 0; +- const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; +- setOpen(false); ++function AgentSessionRow({ session, agent, actions, renaming, index }) { ++ const input = (0, import_react19.useRef)(null); ++ (0, import_react19.useLayoutEffect)(() => { ++ if (!renaming) return; ++ input.current?.focus(); ++ input.current?.select(); ++ }, [renaming]); ++ if (renaming) { ++ const commit = (keep) => { ++ const value = input.current?.value ?? ""; ++ if (!keep || value.trim() === "" || value.trim() === session.title) { ++ actions.endAgentRename(); ++ return; ++ } ++ void (async () => { ++ try { ++ await actions.renameAgentSession(agent.id, session.id, value); ++ actions.endAgentRename(); ++ } catch (error) { ++ actions.reportError(error, "agent-rename"); ++ } ++ })(); + }; +- const onKey = (event) => { +- if (event.key === "Escape") { ++ return (0, import_react19.createElement)( ++ "div", ++ { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, ++ (0, import_react19.createElement)("input", { ++ ref: input, ++ defaultValue: session.title, ++ "aria-label": t("Rename"), ++ onKeyDown: (event) => { ++ if (event.nativeEvent.isComposing) return; ++ if (event.key === "Enter") commit(true); ++ else if (event.key === "Escape") commit(false); ++ }, ++ onBlur: () => commit(true) ++ }) ++ ); ++ } ++ return (0, import_react19.createElement)( ++ "button", ++ { ++ type: "button", ++ className: "bt-agent-sub-row", ++ "aria-current": session.active ? "page" : void 0, ++ style: { "--i": index }, ++ onClick: () => actions.openAgentSession(session.id), ++ onContextMenu: (event) => { ++ event.preventDefault(); + event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); ++ actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); + } ++ }, ++ (0, import_react19.createElement)("span", { className: "bt-agent-sub-title" }, session.title), ++ (0, import_react19.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ++ ); ++} ++function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { ++ const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { ++ const row = list.byId[id]; ++ return { ++ id, ++ // A blank Session's title is the workspace folder name; show the label instead. ++ title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", ++ at: row?.updatedAt ?? 0, ++ running: row?.running === true, ++ active: (row?.retainedBy?.mainView ?? 0) > 0 + }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +- const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); +- setOpen((value) => !value); +- }; +- const groups = [ +- [ +- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], - [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], - [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], - [t("Bot settings"), GearIcon, () => actions.openSettings("general")] -+ [t("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")] - ] - ]; +- ], +- [ +- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] +- ] +- ]; - return (0, import_react8.createElement)( - import_react8.Fragment, -+ return (0, import_react19.createElement)( -+ import_react19.Fragment, - null, +- null, - (0, import_react8.createElement)( ++ }).sort((a, b) => b.at - a.at))); ++ const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ ++ id, ++ running: map.get(id)?.running === true, ++ unread: map.get(id)?.completionUnread === true ++ })))); ++ const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); ++ const sessions = JSON.parse(info).map((session) => ({ ++ ...session, ++ running: statuses[session.id]?.running ?? session.running, ++ unread: statuses[session.id]?.unread ?? false, ++ title: session.title === "" ? t("New session") : session.title ++ })); ++ const renaming = useUi((value) => value.renameAgent); ++ const active = sessions.some((session) => session.active); ++ const running = sessions.some((session) => session.running); ++ const unread = sessions.some((session) => session.unread) || marked && !active; ++ useMarkedUnread(agent.id, active, marked, actions); ++ const preview = sessions[0]?.title ?? t("New session"); ++ const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); ++ const open = active && wide; ++ return (0, import_react19.createElement)( ++ "div", ++ { className: "bt-agent" }, + (0, import_react19.createElement)( "button", - { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, +- { 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 })) -+ (0, import_react19.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react19.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ { ++ type: "button", ++ className: "bt-row", ++ "aria-label": label, ++ "aria-current": active ? "page" : void 0, ++ "data-agent-id": agent.id, ++ title: wide ? void 0 : "DSH Agent", ++ onClick: () => actions.openAgent(agent.id), ++ onContextMenu: (event) => { ++ event.preventDefault(); ++ actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); ++ } ++ }, ++ (0, import_react19.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react19.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), ++ wide ? (0, import_react19.createElement)( ++ "span", ++ { className: "bt-row-body" }, ++ (0, import_react19.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), ++ preview ? (0, import_react19.createElement)("span", { className: "bt-row-preview" }, preview) : null ++ ) : null, ++ wide && unread && !active ? (0, import_react19.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ), - open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -+ open && box ? (0, import_react_dom4.createPortal)((0, import_react19.createElement)( ++ (0, import_react19.createElement)( "div", - { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, +- { 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, -+ (0, import_react19.createElement)(FlyBotAccountHeader, { actions }), -+ groups.map((group, index) => (0, import_react19.createElement)( -+ import_react19.Fragment, - { key: index }, +- { 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", { -+ index > 0 ? (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, hint]) => (0, import_react19.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")); - } +- key: label, +- type: "button", +- role: "menuitem", +- onClick: () => { +- setOpen(false); +- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); +- } - }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) -+ }, (0, import_react19.createElement)(Icon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react19.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) - )) - ), document.body) : null +- )) +- ), document.body) : null ++ { className: "bt-agent-sub", "data-open": open || void 0 }, ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-agent-sub-in" }, ++ (0, import_react19.createElement)( ++ "button", ++ { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { ++ void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); ++ } }, ++ (0, import_react19.createElement)(PlusIcon), ++ (0, import_react19.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) ++ ), ++ sessions.map((session, index) => (0, import_react19.createElement)(AgentSessionRow, { ++ key: session.id, ++ session, ++ agent, ++ actions, ++ index: index + 1, ++ renaming: renaming === session.id ++ })) ++ ) ++ ) ); -@@ -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}` : ""; - }); + } +-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)(() => { -+ const lastBot = (0, import_react19.useRef)(null); -+ (0, import_react19.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; +- if (!roster.ready || !roster.mainBotId) return void 0; +- const [id, blank3] = mainView.split("|"); +- const bot = id === "" ? void 0 : roster.byId[id]; +- if (bot !== void 0) { +- lastBot.current = { botId: bot.id, sessionId: id }; +- if (currentPart(bot) !== id) actions.landOn(bot.id); +- return void 0; +- } +- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { +- lastBot.current = null; +- return void 0; ++var GLIDE = springEasing(1e3, 63); ++function useListGlide(listRef, layout, key) { ++ const tops = (0, import_react19.useRef)({ layout, map: /* @__PURE__ */ new Map() }); ++ (0, import_react19.useLayoutEffect)(() => { ++ const list = listRef.current; ++ if (!list) return; ++ const next = /* @__PURE__ */ new Map(); ++ const still = reducedMotion() || tops.current.layout !== layout; ++ for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { ++ const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; ++ const top = row.offsetTop; ++ next.set(id, top); ++ const before = tops.current.map.get(id); ++ if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { ++ row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); ++ } } - const last = lastBot.current; +- const last = lastBot.current; - const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); -+ 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 () => 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 (0, import_react19.createElement)("span", { className: "bt-foot-account" }, (0, import_react19.createElement)(AccountLauncher, { ...props, seat: false })); - } - function BotReturn({ wide, useSurface, actions }) { - const mode = useSurface((value) => value.mode); - if (mode !== "agent") return null; +-} +-function BotReturn({ wide, useSurface, actions }) { +- const mode = useSurface((value) => value.mode); +- if (mode !== "agent") return null; - return (0, import_react8.createElement)( -+ return (0, import_react19.createElement)( - "button", +- "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 -+ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, -+ (0, import_react19.createElement)(FlyBotMark, { size: 20 }), -+ wide ? (0, import_react19.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)} +- ); +-} +-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} -+.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 }) { +-/* 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 })); -+ return (0, import_react19.createElement)("span", { className: "bt-brand-mark" }, (0, import_react19.createElement)(FlyBotMark, { size })); - } - function BrandName() { +-} +-function BrandName() { - return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); -+ return (0, import_react19.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); - } - var SIDEBAR_CSS = ` - .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5440,6 +5440,10 @@ var SIDEBAR_CSS = ` - .bt-rail .bt-row{position:relative} - .bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} - .bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} -+.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} -+.bt-read-only-reload:hover{background:var(--bt-hover)} -+.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} - [class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} - [class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} - .bt-foot-account{order:-1;flex:none;display:inline-flex} -@@ -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)} +-} +-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}} -+.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)} -@@ -5481,1689 +5481,2804 @@ var SIDEBAR_CSS = ` - .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)} +-@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)} @@ -13446,7 +13656,9 @@ diff --git a/client.js b/client.js - return () => clearTimeout(timer); - }, [seen]); - return shown; --} ++ tops.current = { layout, map: next }; ++ }, [layout, key]); + } -function MemoryNotice({ shown }) { - const inner = (0, import_react9.useRef)(null); - const [width, setWidth] = (0, import_react9.useState)(0); @@ -13458,10 +13670,46 @@ diff --git a/client.js b/client.js - "span", - { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, - (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) -- ); --} ++var NodeActions = class extends import_react19.Component { ++ constructor(props) { ++ super(props); ++ this.state = { failed: false }; ++ } ++ static getDerivedStateFromError() { ++ return { failed: true }; ++ } ++ componentDidCatch(error) { ++ console.error("flybot: nodeActions failed", error); ++ } ++ render() { ++ return this.state.failed ? null : (0, import_react19.createElement)(this.props.component, { bot: this.props.bot }); ++ } ++}; ++function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { ++ const { bot, depth, children } = node; ++ const closed = collapsed[bot.id] === true; ++ return (0, import_react19.createElement)( ++ import_react19.Fragment, ++ null, ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, ++ children.length ? (0, import_react19.createElement)( ++ "button", ++ { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, ++ (0, import_react19.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") ++ ) : (0, import_react19.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), ++ (0, import_react19.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), ++ typeof actions === "function" ? (0, import_react19.createElement)(NodeActions, { component: actions, bot }) : null ++ ), ++ closed ? null : children.map((child) => (0, import_react19.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) + ); + } -function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { -- const roster = useRoster((value) => value); ++function TeamSidebar(props) { ++ const { wide, useRoster, useUi, useSessions, actions } = props; ++ const listRef = (0, import_react19.useRef)(null); + const roster = useRoster((value) => value); - const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); - const firstTurns = useSessions((list) => { - const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; @@ -13472,7 +13720,13 @@ diff --git a/client.js b/client.js - const opening = useTurnClock((state) => { - const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); - return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -- }); ++ const markedUnread = useUi((value) => value.unread); ++ props.useRegistry((value) => value); ++ const updated = useSessions((list) => { ++ const stamps = {}; ++ for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; ++ return JSON.stringify(stamps); + }); - (0, import_react9.useEffect)(() => { - if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); - else document.body.style.removeProperty("--bt-first-sep"); @@ -13505,7 +13759,45 @@ diff --git a/client.js b/client.js - }, [Boolean(room2)]); - const memoryNotice = notice && changed !== null && roster.memory !== false; - return (0, import_react9.createElement)( -- "div", ++ const order = (0, import_react19.useMemo)(() => { ++ const stamps = JSON.parse(updated); ++ const entries2 = [ ++ // Own Bots, friends' Bots, groups and face rooms share one list. A remote chat ++ // still uses BotRow's globe; gone / hidden / home-level remotes stay out. ++ ...roster.bots.filter((bot) => bot.id !== roster.mainBotId && isShownRemoteChat(bot)).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), ++ ...roster.rooms.filter((room2) => !isPastFaceRoom(room2)).map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), ++ // The Agent sorts by its most recent Session, falling back to when it was added. ++ ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) ++ ]; ++ const foreign = (entry) => entry.kind === "bot" && entry.item.remote || entry.kind === "room" && (entry.item.origin === "face-to-face" || entry.item.sharedFrom); ++ return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || Number(foreign(a)) - Number(foreign(b)) || b.at - a.at); ++ }, [roster, updated]); ++ const main = roster.byId[roster.mainBotId]; ++ const readOnlySaid = typeof roster.readOnly === "string" && roster.readOnly.trim() !== ""; ++ const readOnlyReload = (0, import_react19.createElement)("button", { ++ type: "button", ++ className: "bt-read-only-reload", ++ onClick: () => { ++ void Promise.resolve(actions.reloadTeam?.()).catch((error) => actions.reportError(error, "reload")); ++ } ++ }, t("Reload")); ++ const structure = useSidebarStructure(); ++ const [collapsed, setCollapsed] = (0, import_react19.useState)({}); ++ const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); ++ const inTree = (entry) => entry.kind === "bot" && !entry.item.remote; ++ const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false; ++ const tree = structure && wide && main ? botTree([main, ...order.filter(listed).map((entry) => entry.item)], main.id, (id) => structure.parentOf(id)) : null; ++ useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); ++ useLandOnMainBot(roster, useSessions, actions); ++ const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; ++ const typeToSearch = (event) => { ++ if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; ++ if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; ++ event.preventDefault(); ++ actions.openPalette(event.key); ++ }; ++ return (0, import_react19.createElement)( + "div", - { className: "bt-head" }, - (0, import_react9.createElement)( - "button", @@ -13521,12 +13813,39 @@ diff --git a/client.js b/client.js - (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), - (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), - (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) -- ), ++ { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, ++ wide ? (0, import_react19.createElement)( ++ "div", ++ { className: "bt-side-top" }, ++ (0, import_react19.createElement)("span", { className: "bt-brand" }, (0, import_react19.createElement)(BrandMark, { size: 22 }), (0, import_react19.createElement)(BrandName)), ++ (0, import_react19.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react19.createElement)(SearchIcon)), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react19.createElement)(PlusIcon)) ++ ) : (0, import_react19.createElement)( ++ import_react19.Fragment, ++ null, ++ (0, import_react19.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react19.createElement)(PlusIcon)) + ), - (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), - (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), - (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) -- ); --} ++ wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, ++ wide && readOnlySaid ? readOnlyReload : null, ++ !wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, ++ !wide && readOnlySaid ? readOnlyReload : null, ++ (0, import_react19.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), ++ (0, import_react19.createElement)(KanbanRow, { wide, useUi, actions }), ++ (0, import_react19.createElement)(ProjectsNav, { wide }), ++ (0, import_react19.createElement)( ++ "div", ++ { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, ++ tree ? (0, import_react19.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react19.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react19.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react19.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), ++ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react19.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react19.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react19.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), ++ wide ? (0, import_react19.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react19.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null ++ ) + ); + } -function useTranscriptView() { - const [view, setView] = (0, import_react9.useState)(null); - (0, import_react9.useEffect)(() => { @@ -13562,22 +13881,123 @@ diff --git a/client.js b/client.js - bottom: Math.round(window.innerHeight - box.bottom + composer + 8) - }; - setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); -- }; ++function UpdateButton({ useUpdate, actions }) { ++ const update = useUpdate((value) => value); ++ const [open, setOpen] = (0, import_react19.useState)(false); ++ const [box, setBox] = (0, import_react19.useState)(null); ++ const [error, setError] = (0, import_react19.useState)(""); ++ const [dshError, setDshError] = (0, import_react19.useState)(""); ++ const button = (0, import_react19.useRef)(null); ++ (0, import_react19.useEffect)(() => { ++ if (!open) return void 0; ++ const onDown = (event) => { ++ if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; ++ setOpen(false); + }; - const schedule = () => { - if (!frame) frame = requestAnimationFrame(measure); -- }; ++ const onKey = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ setOpen(false); ++ button.current?.focus(); ++ } + }; - document.addEventListener("scroll", schedule, true); - window.addEventListener("resize", schedule); - schedule(); -- return () => { ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); + return () => { - cancelAnimationFrame(frame); - observer.disconnect(); - document.removeEventListener("scroll", schedule, true); - window.removeEventListener("resize", schedule); -- }; ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); + }; - }, []); - return view; --} ++ }, [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 FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); ++ return (0, import_react19.createElement)( ++ import_react19.Fragment, ++ null, ++ (0, import_react19.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_react19.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react19.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), ++ open && box ? (0, import_react_dom4.createPortal)((0, import_react19.createElement)( ++ "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_react19.createElement)( ++ import_react19.Fragment, ++ null, ++ (0, import_react19.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), ++ (0, import_react19.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_react19.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-update-actions" }, ++ update.notesUrl ? (0, import_react19.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, ++ phase === "installed" ? null : (0, import_react19.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_react19.createElement)( ++ "div", ++ { className: "bt-update-dsh" }, ++ (0, import_react19.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), ++ (0, import_react19.createElement)("div", { className: "bt-update-text" }, dshText), ++ dshMessage ? (0, import_react19.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, ++ (0, import_react19.createElement)( ++ "div", ++ { className: "bt-update-actions" }, ++ dsh.kind === "app" ? dsh.available ? (0, import_react19.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_react19.createElement)("button", { ++ type: "button", ++ className: "bt-update-go", ++ disabled: dshPhase === "installing", ++ onClick: startDsh ++ }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) ++ ) ++ ) : null ++ ), document.body) : null ++ ); + } -function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - const replies = useSessions((list) => { - const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; @@ -13589,9 +14009,47 @@ diff --git a/client.js b/client.js - let count = 0; - for (const entry of Object.values(state[sessionId] ?? {})) { - if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; -- } ++function ConnectApps({ wide, actions }) { ++ if (!wide) return null; ++ return (0, import_react19.createElement)( ++ "span", ++ { className: "bt-connect-wrap" }, ++ (0, import_react19.createElement)( ++ "button", ++ { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, ++ (0, import_react19.createElement)(PlugIcon), ++ t("Connect apps") ++ ) ++ ); ++} ++function YouAvatar() { ++ return (0, import_react19.createElement)( ++ "span", ++ { className: "bt-you-wrap" }, ++ (0, import_react19.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react19.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), ++ (0, import_react19.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ ); ++} ++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; + } - return count; -- }); ++ const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); ++ if (desktop) openThroughMenu(desktop); ++ else pressSettingsShortcut(); + }); - const view = useTranscriptView(); - const unpinned = view?.unpinned === true; - const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); @@ -13627,9 +14085,92 @@ diff --git a/client.js b/client.js - className: "bt-news-x", - "aria-label": t("Dismiss new messages"), - onClick: (event) => { -- event.stopPropagation(); ++} ++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_react19.useLayoutEffect)(() => { ++ if (explicit) openSection("models"); ++ complete(); ++ }, []); ++ return null; ++} ++function openHarnessModels() { ++ openHarnessOnboarding(MODELS_STEP_ID); ++} ++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_react19.useState)(false); ++ (0, import_react19.useLayoutEffect)(() => { ++ if (!seat) return void 0; ++ actions.seatAccount(true); ++ return () => actions.seatAccount(false); ++ }, [seat]); ++ const [box, setBox] = (0, import_react19.useState)(null); ++ const button = (0, import_react19.useRef)(null); ++ (0, import_react19.useEffect)(() => { ++ if (settingsOpen) setOpen(false); ++ }, [settingsOpen]); ++ (0, import_react19.useEffect)(() => { ++ if (!openSettings) return void 0; ++ openShellSettings = openSettings; ++ return () => { ++ if (openShellSettings === openSettings) openShellSettings = null; ++ }; ++ }, [openSettings]); ++ (0, import_react19.useEffect)(() => { ++ if (!openOnboarding) return void 0; ++ openShellOnboarding = openOnboarding; ++ return () => { ++ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; ++ }; ++ }, [openOnboarding]); ++ (0, import_react19.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(); - onDismiss(); -- } ++ setOpen(false); ++ button.current?.focus(); + } - }, (0, import_react9.createElement)(CloseIcon)) - ); - const toLatest = () => { @@ -13637,7 +14178,19 @@ diff --git a/client.js b/client.js - const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); - if (back) back.click(); - else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); -- }; ++ }; ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ }; ++ }, [open]); ++ const toggle = () => { ++ const rect = button.current?.getBoundingClientRect(); ++ if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); ++ setOpen((value) => !value); + }; - const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); - return (0, import_react_dom4.createPortal)( - (0, import_react9.createElement)( @@ -13645,10 +14198,48 @@ diff --git a/client.js b/client.js - null, - showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, - showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -- ), ++ const groups = [ ++ [ ++ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], ++ [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_react19.createElement)( ++ import_react19.Fragment, ++ null, ++ (0, import_react19.createElement)( ++ "button", ++ { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, ++ (0, import_react19.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react19.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + ), - document.body -- ); --} ++ open && box ? (0, import_react_dom4.createPortal)((0, import_react19.createElement)( ++ "div", ++ { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, ++ (0, import_react19.createElement)(FlyBotAccountHeader, { actions }), ++ groups.map((group, index) => (0, import_react19.createElement)( ++ import_react19.Fragment, ++ { key: index }, ++ index > 0 ? (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, hint]) => (0, import_react19.createElement)("button", { ++ key: label, ++ type: "button", ++ role: "menuitem", ++ onClick: () => { ++ setOpen(false); ++ void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); ++ } ++ }, (0, import_react19.createElement)(Icon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react19.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) ++ )) ++ ), document.body) : null + ); + } - -// src/client/composer.js -var import_react10 = require("react"); @@ -13672,7 +14263,21 @@ diff --git a/client.js b/client.js - if (!Api) { - setNote(t("Dictation is not available in this browser")); - return false; -- } ++function useLandOnMainBot(roster, useSessions, actions) { ++ const mainView = useSessions((list) => { ++ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); ++ return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; ++ }); ++ const lastBot = (0, import_react19.useRef)(null); ++ (0, import_react19.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; + } - const engine = new Api(); - engine.lang = navigator.language || "zh-CN"; - engine.interimResults = true; @@ -13705,42 +14310,30 @@ diff --git a/client.js b/client.js - } catch { - setNote(t("Couldn't start dictation")); - return false; -- } -- }; -- const stop = () => recognition.current?.stop(); -- return { listening, note, start, stop }; --} --function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -- const dictation = useDictation((text, final) => { -- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); -- }); -- const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -- (0, import_react10.useEffect)(() => { -- if (!quote || !inputActions) return; -- actions.clearQuote(quote.token); -- const excerpt = quote.text.replace(/\s+/g, " ").trim(); -- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} -+.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%} -+.bt-tree-item>.bt-row-wrap{flex:1 1 auto;width:auto;min-width:0} -+/* Gear sits over the row so hover/selected fill the same full-width band as a plain row (#384). */ -+.bt-row-settings{position:absolute;right:6px;top:50%;transform:translateY(-50%);z-index:1;flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:0;pointer-events:none;transition:background-color .12s ease,color .12s ease,opacity .12s ease} -+.bt-row-wrap:hover>.bt-row,.bt-row-wrap:focus-within>.bt-row{padding-right:36px} -+.bt-row-wrap:hover .bt-row-settings,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;pointer-events:auto;color:var(--bt-ink-2);background:var(--bt-hover)} -+.bt-row-settings:hover{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)} -+.bt-row-settings svg{width:15px;height:15px} - `; ++ if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { ++ lastBot.current = null; ++ return void 0; + } +- }; +- const stop = () => recognition.current?.stop(); +- return { listening, note, start, stop }; ++ const last = lastBot.current; ++ const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400); ++ return () => clearTimeout(timer); ++ }, [mainView, roster]); + } +-function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +- const dictation = useDictation((text, final) => { +- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); +- }); +- const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); +- (0, import_react10.useEffect)(() => { +- if (!quote || !inputActions) return; +- actions.clearQuote(quote.token); +- const excerpt = quote.text.replace(/\s+/g, " ").trim(); +- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} +- +-`; - inputActions.insertText(line, inputActions.captureInsertion()); - const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); - if (box instanceof HTMLElement) { @@ -13773,11 +14366,20 @@ diff --git a/client.js b/client.js - onClick: () => actions.openVoice(sessionId) - }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), - dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null -- ); -+ -+// src/client/busy-ack-logic.js -+function busyAckText(hostAck) { -+ return typeof hostAck?.text === "string" ? hostAck.text.trim() : ""; ++function FooterAccount(props) { ++ const seated = props.useUi((value) => value.accountSeated === true); ++ if (seated) return null; ++ return (0, import_react19.createElement)("span", { className: "bt-foot-account" }, (0, import_react19.createElement)(AccountLauncher, { ...props, seat: false })); ++} ++function BotReturn({ wide, useSurface, actions }) { ++ const mode = useSurface((value) => value.mode); ++ if (mode !== "agent") return null; ++ return (0, import_react19.createElement)( ++ "button", ++ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, ++ (0, import_react19.createElement)(FlyBotMark, { size: 20 }), ++ wide ? (0, import_react19.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null + ); } -function menuIcons(roster) { - const marks = /* @__PURE__ */ new Map(); @@ -13787,6 +14389,101 @@ diff --git a/client.js b/client.js - if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); - return marks.get(id); - }; ++var RETURN_CSS = ` ++.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} ++.bt-return:hover{background:rgba(127,127,127,.14)} ++.bt-return:active{transform:scale(.96)} ++.bt-return .bt-mark,.bt-return .bt-flybot-mark{display:inline-flex;flex:none;line-height:0} ++.bt-return .bt-mark svg,.bt-return .bt-flybot-mark{width:20px;height:20px;overflow:visible} ++/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ ++.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} ++.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++`; ++function BrandMark({ size = 24 }) { ++ return (0, import_react19.createElement)("span", { className: "bt-brand-mark" }, (0, import_react19.createElement)(FlyBotMark, { size })); ++} ++function BrandName() { ++ return (0, import_react19.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); ++} ++var SIDEBAR_CSS = ` ++.bt-side-top .bt-brand{cursor:default;padding-left:6px} ++.bt-side-top .bt-brand:hover{background:none} ++.bt-brand .bt-mark{flex:none} ++.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} ++.bt-rail .bt-row{position:relative} ++.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} ++.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} ++.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} ++.bt-read-only-reload:hover{background:var(--bt-hover)} ++.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} ++[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} ++[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} ++.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)} ++.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%} ++.bt-tree-item>.bt-row-wrap{flex:1 1 auto;width:auto;min-width:0} ++/* Gear sits over the row so hover/selected fill the same full-width band as a plain row (#384). */ ++.bt-row-settings{position:absolute;right:6px;top:50%;transform:translateY(-50%);z-index:1;flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:0;pointer-events:none;transition:background-color .12s ease,color .12s ease,opacity .12s ease} ++.bt-row-wrap:hover>.bt-row,.bt-row-wrap:focus-within>.bt-row{padding-right:36px} ++.bt-row-wrap:hover .bt-row-settings,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;pointer-events:auto;color:var(--bt-ink-2);background:var(--bt-hover)} ++.bt-row-settings:hover{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)} ++.bt-row-settings svg{width:15px;height:15px} ++`; ++ ++// src/client/busy-ack-logic.js ++function busyAckText(hostAck) { ++ return typeof hostAck?.text === "string" ? hostAck.text.trim() : ""; ++} +function busyAckPending(hostAck) { + return busyAckText(hostAck) === ""; } @@ -13864,35 +14561,6 @@ diff --git a/client.js b/client.js - } - 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_react10.createElement)( -- import_react10.Fragment, -- null, -- (0, import_react10.createElement)("button", { -- ref: button, -- type: "button", -- className: "bt-plus", -- "aria-label": t("Add"), -- title: t("Add"), -- "aria-haspopup": "menu", -- "aria-expanded": Boolean(open), -- onMouseDown: keepComposerFocus, -- onClick: toggle -- }, (0, import_react10.createElement)(PlusIcon)), -- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( + }, [active]); + return host; +} @@ -13923,20 +14591,7 @@ diff --git a/client.js b/client.js + }, + (0, import_react20.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }), + (0, import_react20.createElement)( - "div", -- { -- ref: menu, -- className: "bt-menu bt-menu-up bt-plus-menu", -- role: "menu", -- "aria-label": t("Add"), -- style: { left: shown.left, bottom: shown.bottom }, -- "data-leaving": leaving || void 0 -- }, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -- ), document.body) : null -- ); ++ "div", + { className: "bt-astack" }, + (0, import_react20.createElement)( + "div", @@ -13955,7 +14610,7 @@ diff --git a/client.js b/client.js + ) + ) + ), host); - } ++} +var BUSY_ACK_CSS = ` +.bt-busy-ack{display:flex;align-items:flex-end;gap:8px;width:100%;margin-top:var(--dsh-chat-flow-gap,12px);margin-bottom:4px} +.bt-busy-ack .bt-astack{align-items:flex-start} @@ -13966,28 +14621,7 @@ diff --git a/client.js b/client.js +@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/at-menu.js --var BOTS_SOURCE = "ds-bot"; --var CHATS_SOURCE = "ds-bot-chats"; --var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; --var OTHER_CHATS_MAX = 8; --var conversationOf = (roster, sessionId) => { -- if (!roster.ready) return null; -- const room2 = roster.roomsById[sessionId]; -- if (room2) return { room: room2 }; -- const self = roster.byId[sessionId]; -- return self ? { self } : null; --}; --function mentionRows(roster, sessionId, query = "") { -- const here = conversationOf(roster, sessionId); -- if (here === null) return null; -- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); -- const needle = query.trim().toLowerCase(); -- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); -- const everyone = t("Everyone"); -- if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -- rows.push({ room: here.room, text: `@${everyone} ` }); ++ +// src/client/project-board.js +var import_react21 = require("react"); +var PROJECT_BOARD_CSS = ` @@ -14123,8 +14757,7 @@ diff --git a/client.js b/client.js + if (stated !== null && typeof stated === "object") { + const id = text(stated.id); + if (id !== "" && (stated.kind === "group" || stated.kind === "bot")) return { kind: stated.kind, id }; - } -- return rows; ++ } + if (task === void 0) return null; + if (row.source === "group") { + const id = text(task.origin?.roomId); @@ -14148,7 +14781,10 @@ diff --git a/client.js b/client.js + title: text(file.title), + sha256: text(file.sha256), + shortHash: text(file.sha256).slice(0, 12) -+ }; + }; +- const attach = () => { +- setOpen(null); +- button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); +}; +var meetingOf = (meeting, status) => { + if (meeting === null || typeof meeting !== "object") return null; @@ -14172,7 +14808,10 @@ diff --git a/client.js b/client.js + // #1861: the host's participants with who each position speaks for, so the row can say a + // Project Chief or an agent held it for someone rather than for themselves. + participants: Array.isArray(meeting.participants) ? meeting.participants.map((participant) => ({ name: text(participant?.name), speaksFor: text(participant?.speaksFor) || null })).filter((participant) => participant.name !== "") : [] -+ }; + }; +- const schedule = () => { +- setOpen(null); +- actions.openSettings("schedules", target); +}; +var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; +function projectBoardRecord({ decisions, meetings } = {}) { @@ -14206,15 +14845,11 @@ diff --git a/client.js b/client.js + empty: decided.length === 0 && pending.length === 0, + emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." + } -+ }; - } --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 reference = () => { +- setOpen(null); +- actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); ++} +function projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics } = {}) { + const isRow = (row) => row !== null && typeof row === "object" && text(row.id) !== ""; + const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); @@ -14247,7 +14882,36 @@ diff --git a/client.js b/client.js + 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, +- null, +- (0, import_react10.createElement)("button", { +- ref: button, +- type: "button", +- className: "bt-plus", +- "aria-label": t("Add"), +- title: t("Add"), +- "aria-haspopup": "menu", +- "aria-expanded": Boolean(open), +- onMouseDown: keepComposerFocus, +- onClick: toggle +- }, (0, import_react10.createElement)(PlusIcon)), +- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( +- "div", +- { +- ref: menu, +- className: "bt-menu bt-menu-up bt-plus-menu", +- role: "menu", +- "aria-label": t("Add"), +- style: { left: shown.left, bottom: shown.bottom }, +- "data-leaving": leaving || void 0 +- }, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), +- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) +- ), document.body) : null + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], @@ -14259,37 +14923,7 @@ diff --git a/client.js b/client.js + 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 team = /* @__PURE__ */ new Set([ -- ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), -- ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) -- ]); -- const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); -- return [...bots, ...others]; --} --function sessionMention(sessionId, label) { -- let binary = ""; -- for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); -- const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -- return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; --} --function atSources({ roster, sessions, launched, iconFor }) { -- const bots = { -- trigger: "@", -- name: BOTS_SOURCE, -- order: -20, -- showGroupTitle: false, -- async candidates(session, { query }) { -- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ -- name: row.bot ? row.bot.name : t("Everyone"), -- ...row.bot?.role ? { description: row.bot.role } : {}, -- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, -- icon: iconFor(row), -- section: t("Bots"), -- value: row.text -- })); -- }, -- onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 ++ }); + 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, waitedMs: Number.isFinite(item.waitedMs) ? item.waitedMs : null })); @@ -14314,26 +14948,7 @@ diff --git a/client.js b/client.js + emptyText: "No cards yet.", + record: projectBoardRecord({ decisions, meetings }), + metrics: metricsView(metrics, { tasks }) - }; -- 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 } }; ++ }; +} +function projectRowNode(row, onOpen) { + const open = row.board === null ? void 0 : () => onOpen?.(row.board); @@ -14351,10 +14966,7 @@ diff --git a/client.js b/client.js + onKeyDown: open === void 0 ? void 0 : (event) => { + if (event.key === "Enter" || event.key === " ") open(); + } - }, -- codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } -- }; -- return [bots, chats]; ++ }, + (0, import_react21.createElement)("span", { className: "bt-project-column" }, t(row.column)), + (0, import_react21.createElement)( + "span", @@ -14370,18 +14982,32 @@ diff --git a/client.js b/client.js + row.speaksFor === null ? null : (0, import_react21.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), + row.priority === null ? null : (0, import_react21.createElement)("span", { className: "bt-project-priority" }, row.priority), + row.runId === null ? null : (0, import_react21.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) -+ ); + ); } - --// 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) }; +-// src/client/at-menu.js +-var BOTS_SOURCE = "ds-bot"; +-var CHATS_SOURCE = "ds-bot-chats"; +-var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; +-var OTHER_CHATS_MAX = 8; +-var conversationOf = (roster, sessionId) => { +- if (!roster.ready) return null; +- const room2 = roster.roomsById[sessionId]; +- if (room2) return { room: room2 }; +- const self = roster.byId[sessionId]; +- return self ? { self } : null; +-}; +-function mentionRows(roster, sessionId, query = "") { +- const here = conversationOf(roster, sessionId); +- if (here === null) return null; +- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); +- const needle = query.trim().toLowerCase(); +- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); +- const everyone = t("Everyone"); +- if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { +- rows.push({ room: here.room, text: `@${everyone} ` }); +- } +- return rows; +function ProjectBoardRows({ view, onOpen }) { + if (view.empty) return (0, import_react21.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); + return view.groups.map((group) => (0, import_react21.createElement)( @@ -14391,27 +15017,13 @@ diff --git a/client.js b/client.js + group.rows.map((row) => projectRowNode(row, onOpen)) + )); } --function waveBytes(samples, rate = 16e3) { -- const bytes = new Uint8Array(44 + samples.length * 2); -- const view = new DataView(bytes.buffer); -- const ascii = (at, text) => { -- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); -- }; -- ascii(0, "RIFF"); -- view.setUint32(4, 36 + samples.length * 2, true); -- ascii(8, "WAVEfmt "); -- view.setUint32(16, 16, true); -- view.setUint16(20, 1, true); -- view.setUint16(22, 1, true); -- view.setUint32(24, rate, true); -- view.setUint32(28, rate * 2, true); -- view.setUint16(32, 2, true); -- view.setUint16(34, 16, true); -- ascii(36, "data"); -- view.setUint32(40, samples.length * 2, true); -- samples.forEach((sample, index) => { -- const value = Math.max(-1, Math.min(1, sample)); -- view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); +-function chatRows(roster, list, sessionId) { +- const here = conversationOf(roster, sessionId); +- if (here === null) return []; +- const byId = list?.byId ?? {}; +- const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { +- const id = currentPart(bot); +- return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; +function ProjectDoneGroup({ view, onOpen }) { + if (view.done.rows.length === 0) return null; + return (0, import_react21.createElement)( @@ -14467,7 +15079,12 @@ diff --git a/client.js b/client.js + board: boardOfRow({ source: text(row.source) }, task) + }; }); -- return bytes; +- const team = /* @__PURE__ */ new Set([ +- ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), +- ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) +- ]); +- const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); +- return [...bots, ...others]; + return { + available: source?.ok === true, + reason: text(source?.reason) || null, @@ -14477,10 +15094,11 @@ diff --git a/client.js b/client.js + emptyText: "No metrics read yet." + }; } --function base64Of(bytes) { -- let text = ""; -- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); -- return btoa(text); +-function sessionMention(sessionId, label) { +- let binary = ""; +- for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); +- const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +- return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; +function ProjectMetrics({ view }) { + return (0, import_react21.createElement)( + "section", @@ -14499,25 +15117,23 @@ diff --git a/client.js b/client.js + ) + ); } --var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); --async function openCapture() { -- const devices = navigator.mediaDevices; -- if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); -- let stream; -- try { -- stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); -- } catch (error) { -- throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); -- } -- const audio = new AudioContext(); -- const analyser = audio.createAnalyser(); -- analyser.fftSize = 512; -- audio.createMediaStreamSource(stream).connect(analyser); -- const frame = new Float32Array(analyser.fftSize); -- const recorder = new MediaRecorder(stream); -- const chunks = []; -- recorder.ondataavailable = (event) => { -- if (event.data.size > 0) chunks.push(event.data); +-function atSources({ roster, sessions, launched, iconFor }) { +- const bots = { +- trigger: "@", +- name: BOTS_SOURCE, +- order: -20, +- showGroupTitle: false, +- async candidates(session, { query }) { +- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ +- name: row.bot ? row.bot.name : t("Everyone"), +- ...row.bot?.role ? { description: row.bot.role } : {}, +- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, +- icon: iconFor(row), +- section: t("Bots"), +- value: row.text +- })); +- }, +- onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 +function ProjectRunningTable({ view, onOpen }) { + if (view.running.length === 0) return (0, import_react21.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react21.createElement)("h4", null, t("Running tasks")), (0, import_react21.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); + return (0, import_react21.createElement)( @@ -14603,15 +15219,25 @@ diff --git a/client.js b/client.js + const settle = (answer) => setOpen((current) => current !== null && current.path === path ? { path, answer } : current); + call4("project-decision", { id: projectId, path }).then(settle, (error) => settle({ ok: false, reason: String(error?.message ?? error) })); }; -- 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(() => { -- }); +- const chats = { +- trigger: "@", +- name: CHATS_SOURCE, +- order: -10, +- showGroupTitle: false, +- async candidates(session) { +- if (!launched(session.sessionId)) return []; +- return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ +- name: row.bot ? row.bot.name : row.label, +- ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, +- icon: row.bot ? iconFor(row) : "session", +- section: row.bot ? t("Bots") : t("Conversations"), +- value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) +- })); +- }, +- onPick({ candidate }) { +- const { sessionId, label } = JSON.parse(candidate.value); +- const mention = sessionMention(sessionId, label); +- return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; + const decisionBody = (file) => { + if (open === null || open.path !== file.path) return null; + const answer = open.answer; @@ -14674,7 +15300,8 @@ diff --git a/client.js b/client.js + "data-decisions": String(decisions.count), + ...decisions.total === null ? {} : { "data-decisions-total": String(decisions.total) }, + "data-meetings": String(meetings.count) -+ }, + }, +- codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } + (0, import_react21.createElement)("h4", null, t("Decisions")), + decisions.empty ? (0, import_react21.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, + moreLine(decisions.hidden, "decisions-more"), @@ -14694,18 +15321,12 @@ diff --git a/client.js b/client.js + 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 { -- 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); ++ return { + chief: botIdOf(project?.chief), + agent: botIdOf(project?.assistant), + roles: { owner, maintainers, members }, @@ -14714,8 +15335,19 @@ diff --git a/client.js b/client.js + // `project-read` record answers no rank, and the panel never re-derives the host's policy. + role: text(project?.role), + choices: idList(project?.choices) -+ }; -+} + }; +- return [bots, chats]; + } +- +-// src/client/speech.js +-var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; +-var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); +-async function speechCatalog(speech) { +- const result = await speech.catalog(); +- if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); +- const catalog = result.value; +- const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); +- return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; +var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react21.createElement)( + "div", + { @@ -14751,17 +15383,12 @@ diff --git a/client.js b/client.js + onClick: (event) => { + event?.stopPropagation(); + onOpenDossier?.({ role, label, botId }); - }, -- cancel: close, -- async finish(maxSeconds) { -- if (closed || recorder.state === "inactive") { -- close(); -- return null; ++ }, + onKeyDown: (event) => { + if (event.key === "Enter" || event.key === " ") { + event?.stopPropagation(); + onOpenDossier?.({ role, label, botId }); - } ++ } + } + }, t("Dossier")) +); @@ -14804,7 +15431,30 @@ diff --git a/client.js b/client.js + ) + ) + ); -+} + } +-function waveBytes(samples, rate = 16e3) { +- const bytes = new Uint8Array(44 + samples.length * 2); +- const view = new DataView(bytes.buffer); +- const ascii = (at, text) => { +- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); +- }; +- ascii(0, "RIFF"); +- view.setUint32(4, 36 + samples.length * 2, true); +- ascii(8, "WAVEfmt "); +- view.setUint32(16, 16, true); +- view.setUint16(20, 1, true); +- view.setUint16(22, 1, true); +- view.setUint32(24, rate, true); +- view.setUint32(28, rate * 2, true); +- view.setUint16(32, 2, true); +- view.setUint16(34, 16, true); +- ascii(36, "data"); +- view.setUint32(40, samples.length * 2, true); +- samples.forEach((sample, index) => { +- const value = Math.max(-1, Math.min(1, sample)); +- view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); +- }); +- return bytes; +function dossierRowOf(team, botId) { + const id = text(botId); + if (id === "") return null; @@ -14854,7 +15504,11 @@ diff --git a/client.js b/client.js + (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) + ); -+} + } +-function base64Of(bytes) { +- let text = ""; +- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); +- return btoa(text); +function projectAgentFor(bot, selfId) { + if (bot === null || typeof bot !== "object" || bot.remote !== void 0) return null; + const projectId = text(bot.projectId); @@ -14870,29 +15524,12 @@ diff --git a/client.js b/client.js + const open = async () => { + let target = agentId; + if (target === "") { - try { -- await new Promise((resolve) => { -- recorder.onstop = resolve; -- recorder.stop(); -- }); -- stream.getTracks().forEach((track) => track.stop()); -- const blob = new Blob(chunks, { type: recorder.mimeType }); -- if (blob.size === 0) return null; -- const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); -- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); -- const offline = new OfflineAudioContext(1, length, 16e3); -- const source = offline.createBufferSource(); -- source.buffer = decoded; -- source.connect(offline.destination); -- source.start(); -- return waveBytes((await offline.startRendering()).getChannelData(0)); -- } finally { -- close(); ++ try { + const record = await (read ?? call)("project-read", { id }); + target = botIdOf(record?.project?.assistant); + } catch { + target = ""; - } ++ } + if (target !== "") setAgentId(target); + } + if (target !== "") onOpen?.(target); @@ -14940,7 +15577,26 @@ diff --git a/client.js b/client.js + (0, import_react21.createElement)(ProjectDoneGroup, { view, onOpen }), + (0, import_react21.createElement)(ProjectBoardRecord, { record: view.record, projectId: view.projectId, call: call4 }) + ); -+} + } +-var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); +-async function openCapture() { +- const devices = navigator.mediaDevices; +- if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); +- let stream; +- try { +- stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); +- } catch (error) { +- throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); +- } +- const audio = new AudioContext(); +- const analyser = audio.createAnalyser(); +- analyser.fftSize = 512; +- audio.createMediaStreamSource(stream).connect(analyser); +- const frame = new Float32Array(analyser.fftSize); +- const recorder = new MediaRecorder(stream); +- const chunks = []; +- recorder.ondataavailable = (event) => { +- if (event.data.size > 0) chunks.push(event.data); +function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { + const save = request ?? call; + const [kind, setKind] = (0, import_react21.useState)("bot"); @@ -14961,8 +15617,17 @@ diff --git a/client.js b/client.js + } catch (error) { + setStatus(hostText(error?.message ?? String(error))); + return null; - } ++ } }; +- recorder.start(); +- let closed = false; +- const close = () => { +- if (closed) return; +- closed = true; +- if (recorder.state !== "inactive") recorder.stop(); +- stream.getTracks().forEach((track) => track.stop()); +- void audio.close().catch(() => { +- }); + return (0, import_react21.createElement)( + "div", + { className: "bt-project-panel", "data-panel": "project-settings" }, @@ -15034,7 +15699,7 @@ diff --git a/client.js b/client.js + const add = (id) => { + const clean = text(id); + if (clean !== "" && !owners.includes(clean)) owners.push(clean); -+ }; + }; + add(project?.ownerUserId); + for (const id of Array.isArray(project?.ownerUserIds) ? project.ownerUserIds : []) add(id); + const boardMembers = []; @@ -15077,12 +15742,7 @@ diff --git a/client.js b/client.js + (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) + ); - } --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(); ++} +async function readProjectBoard(read, wanted, knownTeam) { + const listing = await read("projects"); + const projects = Array.isArray(listing?.projects) ? listing.projects : []; @@ -15105,7 +15765,20 @@ diff --git a/client.js b/client.js + const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null }; + const base = record?.project ?? project; + const withRole = { ...base, role: project.role ?? null, choices: Array.isArray(project.choices) ? project.choices : [] }; -+ return { + return { +- level() { +- if (closed) return 0; +- analyser.getFloatTimeDomainData(frame); +- let sum = 0; +- for (const sample of frame) sum += sample * sample; +- return Math.min(1, Math.sqrt(sum / frame.length) * 4); +- }, +- cancel: close, +- async finish(maxSeconds) { +- if (closed || recorder.state === "inactive") { +- close(); +- return null; +- } + project: withRole, + assistantMine: record?.assistantMine ?? null, + rows: board?.tasks ?? [], @@ -15119,15 +15792,7 @@ diff --git a/client.js b/client.js + team: dossier.answer !== null && typeof dossier.answer === "object" ? dossier.answer : { rows: [] }, + teamFresh: dossier.fresh + }; - } --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"; ++} +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 } = {}) { @@ -15173,7 +15838,24 @@ diff --git a/client.js b/client.js + const loadBoard = () => readProjectBoard(read, wanted, team.current); + const refresh = async (first) => { + const gen = teamGen.current; -+ try { + try { +- await new Promise((resolve) => { +- recorder.onstop = resolve; +- recorder.stop(); +- }); +- stream.getTracks().forEach((track) => track.stop()); +- const blob = new Blob(chunks, { type: recorder.mimeType }); +- if (blob.size === 0) return null; +- const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); +- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); +- const offline = new OfflineAudioContext(1, length, 16e3); +- const source = offline.createBufferSource(); +- source.buffer = decoded; +- source.connect(offline.destination); +- source.start(); +- return waveBytes((await offline.startRendering()).getChannelData(0)); +- } finally { +- close(); + const next = await loadBoard(); + if (!gone) { + const newer = teamGen.current !== gen; @@ -15182,7 +15864,7 @@ diff --git a/client.js b/client.js + } + } catch { + if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); -+ } + } + }; + void refresh(true); + const timer = setInterval(() => { @@ -15207,9 +15889,6 @@ diff --git a/client.js b/client.js + setState((previous) => previous === null ? previous : { ...previous, team: answer }); + } catch { } -- if (!spokeAt) return "waiting"; -- if (level < quiet) quietSince ||= now; -- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; }; + if (state === null) return null; + return (0, import_react21.createElement)( @@ -15282,19 +15961,12 @@ diff --git a/client.js b/client.js + ) + ); } - --// 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); +-async function transcribeWave(speech, catalog, wave, signal) { +- if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); +- const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); +- if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); +- return String(result.value?.text ?? "").trim(); ++ +// src/client/header.js +var import_react22 = require("react"); +var import_react_dom6 = require("react-dom"); @@ -15312,7 +15984,20 @@ diff --git a/client.js b/client.js + return () => clearTimeout(timer); + }, [seen]); + return shown; -+} + } +-function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +- let spokeAt = 0; +- let quietSince = 0; +- return (level, now) => { +- if (level >= loud) { +- spokeAt ||= now; +- quietSince = 0; +- return "speech"; +- } +- if (!spokeAt) return "waiting"; +- if (level < quiet) quietSince ||= now; +- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; +- }; +function MemoryNotice({ shown }) { + const inner = (0, import_react22.useRef)(null); + const [width, setWidth] = (0, import_react22.useState)(0); @@ -15325,7 +16010,20 @@ diff --git a/client.js b/client.js + { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, + (0, import_react22.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react22.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react22.createElement)(MemoryIcon)), label) + ); -+} + } +- +-// src/client/overlay-hooks.js +-var import_react11 = require("react"); +-var escapeLayers = []; +-var onEscapeKey = (event) => { +- if (event.key === "Escape") escapeLayers.at(-1)?.current(); +-}; +-function useEscape(onClose) { +- const ref = (0, import_react11.useRef)(onClose); +- ref.current = onClose; +- (0, import_react11.useEffect)(() => { +- if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); +- escapeLayers.push(ref); +function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useBusyAcks, useMemoryNews, useTurnClock, useUi, useLive, actions }) { + const roster = useRoster((value) => value); + const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); @@ -15379,151 +16077,77 @@ diff --git a/client.js b/client.js + ), + (0, import_react22.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + projectAgent === null ? null : (0, import_react22.createElement)(ProjectAgentEntry, { -+ projectId: projectAgent.projectId, -+ read: call, -+ onOpen: (id) => actions?.openSession?.(id) -+ }), -+ (0, import_react22.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), -+ (0, import_react22.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), -+ (0, import_react22.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) -+ ); -+} -+function useTranscriptView() { -+ const [view, setView] = (0, import_react22.useState)(null); -+ (0, import_react22.useEffect)(() => { -+ let frame = 0; -+ let watched = null; -+ const observer = new MutationObserver(() => schedule()); -+ const measure = () => { -+ frame = 0; -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ if (scroller !== watched) { -+ observer.disconnect(); -+ watched = scroller; -+ if (scroller) observer.observe(scroller, { childList: true, subtree: true }); -+ } -+ if (!scroller) { -+ setView(null); -+ return; -+ } -+ const box = scroller.getBoundingClientRect(); -+ const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; -+ const sep = scroller.querySelector(".bt-new-sep"); -+ let divider = null; -+ if (sep) { -+ const at = sep.getBoundingClientRect(); -+ divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; -+ } -+ const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; -+ const next = { -+ unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, -+ divider, -+ center: Math.round(box.left + box.width / 2), -+ top: Math.round(headerBottom + 8), -+ bottom: Math.round(window.innerHeight - box.bottom + composer + 8) -+ }; -+ setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); -+ }; -+ const schedule = () => { -+ if (!frame) frame = requestAnimationFrame(measure); -+ }; -+ document.addEventListener("scroll", schedule, true); -+ window.addEventListener("resize", schedule); -+ schedule(); - return () => { -- escapeLayers.splice(escapeLayers.indexOf(ref), 1); -- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ document.removeEventListener("scroll", schedule, true); -+ window.removeEventListener("resize", schedule); - }; - }, []); -+ return view; - } --function usePaneLeft() { -- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); -- (0, import_react11.useLayoutEffect)(() => { -- const column = document.querySelector('[class*="_footArea"]')?.parentElement; -- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); -- }, []); -- return paneLeft; -+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_react22.useState)({ base: null, dismissed: false }); -+ const [above, setAbove] = (0, import_react22.useState)({ seen: false, dismissed: false }); -+ (0, import_react22.useEffect)(() => { -+ setRelease({ base: unpinned ? replies : null, dismissed: false }); -+ }, [unpinned]); -+ (0, import_react22.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_react22.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_react22.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_react22.createElement)( -+ "div", -+ { className: "bt-news", "data-direction": direction, style }, -+ (0, import_react22.createElement)( -+ "button", -+ { type: "button", className: "bt-news-jump", onClick: onJump }, -+ (0, import_react22.createElement)("span", { className: "bt-news-ico" }, (0, import_react22.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -+ label(count) -+ ), -+ (0, import_react22.createElement)("button", { -+ type: "button", -+ className: "bt-news-x", -+ "aria-label": t("Dismiss new messages"), -+ onClick: (event) => { -+ event.stopPropagation(); -+ onDismiss(); -+ } -+ }, (0, import_react22.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_react22.createElement)( -+ import_react22.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 ++ projectId: projectAgent.projectId, ++ read: call, ++ onOpen: (id) => actions?.openSession?.(id) ++ }), ++ (0, import_react22.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), ++ (0, import_react22.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), ++ (0, import_react22.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) + ); ++} ++function useTranscriptView() { ++ const [view, setView] = (0, import_react22.useState)(null); ++ (0, import_react22.useEffect)(() => { ++ let frame = 0; ++ let watched = null; ++ const observer = new MutationObserver(() => schedule()); ++ const measure = () => { ++ frame = 0; ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ if (scroller !== watched) { ++ observer.disconnect(); ++ watched = scroller; ++ if (scroller) observer.observe(scroller, { childList: true, subtree: true }); ++ } ++ if (!scroller) { ++ setView(null); ++ return; ++ } ++ const box = scroller.getBoundingClientRect(); ++ const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; ++ const sep = scroller.querySelector(".bt-new-sep"); ++ let divider = null; ++ if (sep) { ++ const at = sep.getBoundingClientRect(); ++ divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; ++ } ++ const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; ++ const next = { ++ unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, ++ divider, ++ center: Math.round(box.left + box.width / 2), ++ top: Math.round(headerBottom + 8), ++ bottom: Math.round(window.innerHeight - box.bottom + composer + 8) ++ }; ++ setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); ++ }; ++ const schedule = () => { ++ if (!frame) frame = requestAnimationFrame(measure); ++ }; ++ document.addEventListener("scroll", schedule, true); ++ window.addEventListener("resize", schedule); ++ schedule(); + return () => { +- escapeLayers.splice(escapeLayers.indexOf(ref), 1); +- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ document.removeEventListener("scroll", schedule, true); ++ window.removeEventListener("resize", schedule); + }; + }, []); ++ return view; } - +-function usePaneLeft() { +- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); +- (0, import_react11.useLayoutEffect)(() => { +- const column = document.querySelector('[class*="_footArea"]')?.parentElement; +- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); +- }, []); +- return paneLeft; +-} +- -// src/client/palette.js -var import_react12 = require("react"); -var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -15558,23 +16182,22 @@ diff --git a/client.js b/client.js - hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); - break; - } -+// 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); - } +- } ++function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }) { ++ const replies = useSessions((list) => { ++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; ++ return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const unread = useTurnClock((state) => { ++ if (typeof visit?.seen !== "number") return 0; ++ let count = 0; ++ for (const entry of Object.values(state[sessionId] ?? {})) { ++ if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; } - return JSON.stringify(hits); -- }); ++ return count; + }); - const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); - const sections = []; - const people = [ @@ -15598,18 +16221,47 @@ diff --git a/client.js b/client.js - run: async () => { - if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); - actions.openSession(room2.id); -+ }; -+ 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 view = useTranscriptView(); ++ const unpinned = view?.unpinned === true; ++ const [release, setRelease] = (0, import_react22.useState)({ base: null, dismissed: false }); ++ const [above, setAbove] = (0, import_react22.useState)({ seen: false, dismissed: false }); ++ (0, import_react22.useEffect)(() => { ++ setRelease({ base: unpinned ? replies : null, dismissed: false }); ++ }, [unpinned]); ++ (0, import_react22.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_react22.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_react22.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_react22.createElement)( ++ "div", ++ { className: "bt-news", "data-direction": direction, style }, ++ (0, import_react22.createElement)( ++ "button", ++ { type: "button", className: "bt-news-jump", onClick: onJump }, ++ (0, import_react22.createElement)("span", { className: "bt-news-ico" }, (0, import_react22.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), ++ label(count) ++ ), ++ (0, import_react22.createElement)("button", { ++ type: "button", ++ className: "bt-news-x", ++ "aria-label": t("Dismiss new messages"), ++ onClick: (event) => { ++ event.stopPropagation(); ++ onDismiss(); } - })) - ]; @@ -15655,7 +16307,13 @@ diff --git a/client.js b/client.js - if (!row || leaving) return; - void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); - close(); -+ } ++ }, (0, import_react22.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" }); }; - let index = -1; - return (0, import_react12.createElement)( @@ -15744,7 +16402,16 @@ diff --git a/client.js b/client.js - )) - ) - ) -- ); ++ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); ++ return (0, import_react_dom6.createPortal)( ++ (0, import_react22.createElement)( ++ import_react22.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 + ); } -// src/client/model-menu.js @@ -15775,7 +16442,37 @@ diff --git a/client.js b/client.js - const served = servedOf(roster, ref); - const [provider, ...rest] = ref.split("/"); - return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; --} ++// src/client/composer-label.js ++var ATTRIBUTES = ["aria-label", "data-placeholder"]; ++function holdComposerLabel(doc, label) { ++ 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 effortCache = /* @__PURE__ */ new Map(); -function useEfforts(served, ref, actions) { - const [fetched, setFetched] = (0, import_react13.useState)(void 0); @@ -15789,6 +16486,7 @@ diff --git a/client.js b/client.js - effortCache.set(ref, reasoning); - if (live) setFetched(reasoning); - }).catch(() => { ++ +// src/client/header-effects.js +function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { + if (!doc) return () => { @@ -15820,6 +16518,9 @@ diff --git a/client.js b/client.js }); - return () => { - live = false; +- }; +- }, [ref, served?.reasoning !== void 0, actions]); +- return served?.reasoning ?? fetched; + run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { + if (!bot?.remote || row?.blank !== true) return void 0; + doc.body.dataset.btRemoteChat = "empty"; @@ -15873,7 +16574,13 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; -+} + } +-function effortRows(reasoning) { +- if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; +- return [ +- { effort: void 0, label: t("Default") }, +- ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) +- ]; + +// src/client/composer.js +var import_react23 = require("react"); @@ -15921,9 +16628,7 @@ diff --git a/client.js b/client.js + setListening(false); + recognition.current = null; + onEnd?.(); - }; -- }, [ref, served?.reasoning !== void 0, actions]); -- return served?.reasoning ?? fetched; ++ }; + recognition.current = engine; + try { + engine.start(); @@ -15937,12 +16642,9 @@ diff --git a/client.js b/client.js + const stop = () => recognition.current?.stop(); + return { listening, note, start, stop }; } --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_react13.createElement)( +- import_react13.Fragment, +function ComposerIdentity({ sessionId, useRoster }) { + const roster = useRoster((value) => value); + const room2 = roster.roomsById?.[sessionId]; @@ -15961,10 +16663,7 @@ diff --git a/client.js b/client.js + { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, + t("Sending as {name}", { name: self.displayName || self.memberId }) + ); - } --function EffortRows({ reasoning, current, onPick }) { -- return (0, import_react13.createElement)( -- import_react13.Fragment, ++} +function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + const dictation = useDictation((text2, final) => { + if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); @@ -16234,6 +16933,59 @@ diff --git a/client.js b/client.js - setError(failure?.message ?? String(failure)); - void actions.refreshModels?.(); - }).finally(() => setBusy("")); +- }; +- const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); +- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ +- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( +- "div", +- { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, +- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), +- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { +- key: model.ref, +- type: "button", +- className: "bt-model-chip", +- disabled: busy !== "", +- "aria-busy": busy === model.ref || void 0, +- "aria-label": `${model.name} · ${provider}`, +- title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, +- onClick: () => pick2(model.ref) +- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) +- )), +- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) +- ]; +- return (0, import_react13.createElement)( +- "div", +- { className: "bt-model-dock" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-astack", "data-lead": "" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-msg" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-msg-line" }, +- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) +- ) +- ) +- ), +- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), +- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null +- ); +-} +-function setupChoices(actions) { +- return [ +- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, +- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], +- [t("Open settings to set up a custom model"), openHarnessSettings] +- ].filter(Boolean); +-} +-function DeepSeekSignIn({ useUi, actions }) { +- const open = useUi((state) => state.signIn); +- const entry = open ? actions.signInEntry?.() : null; +- if (!entry) return null; +- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); + +// src/client/at-menu.js +var BOTS_SOURCE = "ds-bot"; @@ -16281,7 +17033,21 @@ diff --git a/client.js b/client.js + ...rows.filter((row) => !(row.bot && remoteHandles.has(normHandle(remoteRecord(row.bot)?.handle)))), + ...remoteMentionRows(roster, query) + ]; -+} + } +-function SignInHost({ entry, actions }) { +- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); +- const close = () => actions.closeSignIn(); +- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); +- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); +- return (0, import_react13.createElement)(entry.component, { +- ...ops, +- ...hooks, +- t: actions.accountT(), +- complete: close, +- useApiKey: () => { +- close(); +- openHarnessOnboarding("deepseek-official"); +- } +var isFaceRoom = (room2) => room2?.origin === "face-to-face"; +var memberCount = (count) => t(count === 1 ? "{count} member" : "{count} members", { count }); +var botCount = (count) => count === 1 ? t("1 Bot") : t("{count} Bots", { count }); @@ -16298,7 +17064,7 @@ diff --git a/client.js b/client.js + 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) { @@ -16306,222 +17072,80 @@ diff --git a/client.js b/client.js + if (here === null) return []; + const byId = list?.byId ?? {}; + const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { -+ const id = currentPart(bot); -+ return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; -+ }); -+ const team = /* @__PURE__ */ new Set([ -+ ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), -+ ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) -+ ]); -+ const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); -+ return [...bots, ...others]; -+} -+function sessionMention(sessionId, label) { -+ let binary = ""; -+ for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); -+ const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -+ return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; -+} -+function atSources({ roster, sessions, launched, iconFor }) { -+ const bots = { -+ trigger: "@", -+ name: BOTS_SOURCE, -+ order: -20, -+ showGroupTitle: false, -+ async candidates(session, { query }) { -+ const snapshot = roster.getSnapshot(); -+ const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); -+ const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ -+ // 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 text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); -- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ -- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( -- "div", -- { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, -- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { -- key: model.ref, -- type: "button", -- className: "bt-model-chip", -- disabled: busy !== "", -- "aria-busy": busy === model.ref || void 0, -- "aria-label": `${model.name} · ${provider}`, -- title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, -- onClick: () => pick2(model.ref) -- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -- )), -- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) -- ]; -- return (0, import_react13.createElement)( -- "div", -- { className: "bt-model-dock" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-astack", "data-lead": "" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-msg" }, -- (0, import_react13.createElement)( -- "div", -- { className: "bt-msg-line" }, -- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) -- ) -- ) -- ), -- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -- ); -+ 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]; - } --function setupChoices(actions) { -- return [ -- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, -- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], -- [t("Open settings to set up a custom model"), openHarnessSettings] -- ].filter(Boolean); -+ -+// 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 DeepSeekSignIn({ useUi, actions }) { -- const open = useUi((state) => state.signIn); -- const entry = open ? actions.signInEntry?.() : null; -- if (!entry) return null; -- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); -+function 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 SignInHost({ entry, actions }) { -- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); -- const close = () => actions.closeSignIn(); -- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -- return (0, import_react13.createElement)(entry.component, { -- ...ops, -- ...hooks, -- t: actions.accountT(), -- complete: close, -- useApiKey: () => { -- close(); -- openHarnessOnboarding("deepseek-official"); -- } -- }); -+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 id = currentPart(bot); ++ return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; ++ }); ++ const team = /* @__PURE__ */ new Set([ ++ ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), ++ ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) ++ ]); ++ const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); ++ return [...bots, ...others]; ++} ++function sessionMention(sessionId, label) { ++ let binary = ""; ++ for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); ++ const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); ++ return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; ++} ++function atSources({ roster, sessions, launched, iconFor }) { ++ const bots = { ++ trigger: "@", ++ name: BOTS_SOURCE, ++ order: -20, ++ showGroupTitle: false, ++ async candidates(session, { query }) { ++ const snapshot = roster.getSnapshot(); ++ const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); ++ const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ ++ // 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 unsubscribe = controller.menu.subscribe(apply2); -+ apply2(); -+ return () => { -+ stopped = true; -+ unsubscribe?.(); -+ style.remove(); ++ const chats = { ++ trigger: "@", ++ name: CHATS_SOURCE, ++ order: -10, ++ showGroupTitle: false, ++ async candidates(session) { ++ if (!launched(session.sessionId)) return []; ++ return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ ++ name: row.bot ? row.bot.name : row.label, ++ ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, ++ icon: row.bot ? iconFor(row) : "session", ++ section: row.bot ? t("Bots") : t("Conversations"), ++ value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) ++ })); ++ }, ++ onPick({ candidate }) { ++ const { sessionId, label } = JSON.parse(candidate.value); ++ const mention = sessionMention(sessionId, label); ++ return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; ++ }, ++ codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } + }; ++ return [bots, chats]; } -var MODEL_CSS = ` -.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} @@ -16582,67 +17206,76 @@ diff --git a/client.js b/client.js - setProblem(""); - void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); - open(); -+// 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) }; ++// 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 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); ++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 choose = (path) => { - close(); - if (path !== (value ?? "")) void onPick(path); -+ ascii(0, "RIFF"); -+ view.setUint32(4, 36 + samples.length * 2, true); -+ ascii(8, "WAVEfmt "); -+ view.setUint32(16, 16, true); -+ view.setUint16(20, 1, true); -+ view.setUint16(22, 1, true); -+ view.setUint32(24, rate, true); -+ view.setUint32(28, rate * 2, true); -+ view.setUint16(32, 2, true); -+ view.setUint16(34, 16, true); -+ ascii(36, "data"); -+ view.setUint32(40, samples.length * 2, true); -+ samples.forEach((sample, index) => { -+ const value = Math.max(-1, Math.min(1, sample)); -+ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); -+ }); -+ return bytes; -+} -+function base64Of(bytes) { -+ let text2 = ""; -+ for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); -+ return btoa(text2); -+} -+var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); -+async function openCapture() { -+ const devices = navigator.mediaDevices; -+ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); -+ let stream; -+ try { -+ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); -+ } catch (error) { -+ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); -+ } -+ const audio = new AudioContext(); -+ const analyser = audio.createAnalyser(); -+ analyser.fftSize = 512; -+ audio.createMediaStreamSource(stream).connect(analyser); -+ const frame = new Float32Array(analyser.fftSize); -+ const recorder = new MediaRecorder(stream); -+ const chunks = []; -+ recorder.ondataavailable = (event) => { -+ if (event.data.size > 0) chunks.push(event.data); ++ const 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 browse = (path) => { - setProblem(""); @@ -16650,76 +17283,79 @@ diff --git a/client.js b/client.js - setListing(next); - setView("browse"); - }).catch((failure) => setProblem(failure?.message ?? String(failure))); -+ 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(() => { -+ }); ++ const unsubscribe = controller.menu.subscribe(apply2); ++ apply2(); ++ return () => { ++ stopped = true; ++ unsubscribe?.(); ++ style.remove(); }; - const pickNative = () => { - close(); - void run(Promise.resolve(actions.folderPick()).then((path) => path ? onPick(path) : void 0)); -+ 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(); -+ } -+ } ++} ++ ++// 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); }; - const typed = () => { - const path = draft.trim(); - if (path !== "") choose(path); ++ ascii(0, "RIFF"); ++ view.setUint32(4, 36 + samples.length * 2, true); ++ ascii(8, "WAVEfmt "); ++ view.setUint32(16, 16, true); ++ view.setUint16(20, 1, true); ++ view.setUint16(22, 1, true); ++ view.setUint32(24, rate, true); ++ view.setUint32(28, rate * 2, true); ++ view.setUint16(32, 2, true); ++ view.setUint16(34, 16, true); ++ ascii(36, "data"); ++ view.setUint32(40, samples.length * 2, true); ++ samples.forEach((sample, index) => { ++ const value = Math.max(-1, Math.min(1, sample)); ++ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); ++ }); ++ return bytes; +} -+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 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 turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { -+ let spokeAt = 0; -+ let quietSince = 0; -+ return (level, now) => { -+ if (level >= loud) { -+ spokeAt ||= now; -+ quietSince = 0; -+ return "speech"; -+ } -+ if (!spokeAt) return "waiting"; -+ if (level < quiet) quietSince ||= now; -+ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; ++var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); ++async function openCapture() { ++ const devices = navigator.mediaDevices; ++ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); ++ let stream; ++ try { ++ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); ++ } catch (error) { ++ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); ++ } ++ const audio = new AudioContext(); ++ const analyser = audio.createAnalyser(); ++ analyser.fftSize = 512; ++ audio.createMediaStreamSource(stream).connect(analyser); ++ const frame = new Float32Array(analyser.fftSize); ++ const recorder = new MediaRecorder(stream); ++ const chunks = []; ++ recorder.ondataavailable = (event) => { ++ if (event.data.size > 0) chunks.push(event.data); }; - const known = info?.folders ?? []; - const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -16761,7 +17397,30 @@ diff --git a/client.js b/client.js - onChange: (event) => setDraft(event.target.value), - onKeyDown: (event) => { - if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); -- } ++ recorder.start(); ++ let closed = false; ++ const close = () => { ++ if (closed) return; ++ closed = true; ++ if (recorder.state !== "inactive") recorder.stop(); ++ stream.getTracks().forEach((track) => track.stop()); ++ void audio.close().catch(() => { ++ }); ++ }; ++ return { ++ level() { ++ if (closed) return 0; ++ analyser.getFloatTimeDomainData(frame); ++ let sum = 0; ++ for (const sample of frame) sum += sample * sample; ++ return Math.min(1, Math.sqrt(sum / frame.length) * 4); ++ }, ++ cancel: close, ++ async finish(maxSeconds) { ++ if (closed || recorder.state === "inactive") { ++ close(); ++ return null; + } - }), - (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) - ); @@ -16817,6 +17476,47 @@ diff --git a/client.js b/client.js - problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null - ), document.body) : null - ); ++ try { ++ await new Promise((resolve) => { ++ recorder.onstop = resolve; ++ recorder.stop(); ++ }); ++ stream.getTracks().forEach((track) => track.stop()); ++ const blob = new Blob(chunks, { type: recorder.mimeType }); ++ if (blob.size === 0) return null; ++ const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); ++ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); ++ const offline = new OfflineAudioContext(1, length, 16e3); ++ const source = offline.createBufferSource(); ++ source.buffer = decoded; ++ source.connect(offline.destination); ++ source.start(); ++ return waveBytes((await offline.startRendering()).getChannelData(0)); ++ } finally { ++ close(); ++ } ++ } ++ }; ++} ++async function transcribeWave(speech, catalog, wave, signal) { ++ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); ++ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); ++ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); ++ return String(result.value?.text ?? "").trim(); ++} ++function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { ++ let spokeAt = 0; ++ let quietSince = 0; ++ return (level, now) => { ++ if (level >= loud) { ++ spokeAt ||= now; ++ quietSince = 0; ++ return "speech"; ++ } ++ if (!spokeAt) return "waiting"; ++ if (level < quiet) quietSince ||= now; ++ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; ++ }; } -var FOLDER_CSS = ` -.bt-folder-view{display:flex;flex-direction:column;gap:2px;animation:bt-veil-in .14s ease-out} @@ -17292,7 +17992,7 @@ diff --git a/client.js b/client.js + (0, import_react24.createElement)(BotMark, { bot, size: 22 }), + (0, import_react24.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 })) + ) -+ ) + ) + ) + ); +} @@ -17339,15 +18039,23 @@ diff --git a/client.js b/client.js + (0, import_react24.createElement)("span", { className: "bt-turn-error-title" }, title), + (0, import_react24.createElement)("span", { className: "bt-turn-error-message" }, message), + code ? (0, import_react24.createElement)("code", { className: "bt-turn-error-code" }, code) : null -+ ), + ), +- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, +- (0, import_react15.createElement)( + choices.length > 0 ? (0, import_react24.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) +- ) + { className: "bt-turn-error-actions" }, + choices.map(([label, run]) => (0, import_react24.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), + deepseek ? (0, import_react24.createElement)(KeyHelpLink, { key: "key-help" }) : null + ) : null -+ ); -+} + ); + } +-function AutomationOff() { +- return (0, import_react15.createElement)( +function DeepSeekSignIn({ useUi, actions }) { + const open = useUi((state) => state.signIn); + const entry = open ? actions.signInEntry?.() : null; @@ -17463,7 +18171,12 @@ diff --git a/client.js b/client.js + }; + const { busy, outcome } = state; + return (0, import_react25.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-task-off" }, +- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), +- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) + { className: "bt-set-row bt-bs-row bt-model-test" }, + (0, import_react25.createElement)( + "span", @@ -17474,41 +18187,11 @@ diff --git a/client.js b/client.js + { 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_react25.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null - ) - ), -- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -- (0, import_react15.createElement)( -- "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", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ); } --function AutomationOff() { -- return (0, import_react15.createElement)( -- "div", -- { className: "bt-set-card bt-task-off" }, -- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), -- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -- ); -+var 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 SchedulesPage({ roster, actions, start }) { - const view = useSchedules(actions); - const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); @@ -17517,31 +18200,7 @@ diff --git a/client.js b/client.js - const shown = (next) => { - publish(next); - setAdding(false); -+`; -+ -+// src/client/folder-menu.js -+var import_react26 = require("react"); -+var import_react_dom9 = require("react-dom"); -+var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); -+var Tick = ({ on }) => (0, import_react26.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react26.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_react26.useState)(null); -+ const [view, setView] = (0, import_react26.useState)("list"); -+ const [draft, setDraft] = (0, import_react26.useState)(""); -+ const [listing, setListing] = (0, import_react26.useState)(null); -+ const [problem, setProblem] = (0, import_react26.useState)(""); -+ const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); -+ const toggle = () => { -+ if (isOpen) { -+ close(); -+ return; -+ } -+ setView("list"); -+ setProblem(""); -+ void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); -+ open(); - }; +- }; - const 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)]) @@ -17555,7 +18214,20 @@ diff --git a/client.js b/client.js - (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 }))) - )) - ]; --} ++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 BotSchedules({ bot, roster, actions }) { - const view = useSchedules(actions); - if (view === null) return null; @@ -17577,6 +18249,31 @@ diff --git a/client.js b/client.js - (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")) - ] - ) ++`; ++ ++// src/client/folder-menu.js ++var import_react26 = require("react"); ++var import_react_dom9 = require("react-dom"); ++var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); ++var Tick = ({ on }) => (0, import_react26.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react26.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_react26.useState)(null); ++ const [view, setView] = (0, import_react26.useState)("list"); ++ const [draft, setDraft] = (0, import_react26.useState)(""); ++ const [listing, setListing] = (0, import_react26.useState)(null); ++ const [problem, setProblem] = (0, import_react26.useState)(""); ++ const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); ++ const toggle = () => { ++ if (isOpen) { ++ close(); ++ return; ++ } ++ setView("list"); ++ setProblem(""); ++ void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); ++ open(); ++ }; + const choose = (path) => { + close(); + if (path !== (value ?? "")) void onPick(path); @@ -18837,8 +19534,12 @@ diff --git a/client.js b/client.js { 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 }), @@ -18847,8 +19548,9 @@ diff --git a/client.js b/client.js + (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") }, + Card, + { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + onlyMain ? (0, import_react30.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, @@ -19160,20 +19862,15 @@ diff --git a/client.js b/client.js + Card, + { title: t("Working folder") }, + (0, import_react31.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( + (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 }) }) - ) - ), -- (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)( ++ ) ++ ), + face ? (0, import_react31.createElement)( - Card, -- { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.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, @@ -19181,15 +19878,10 @@ diff --git a/client.js b/client.js + Card, + { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, + humans.map((person) => (0, import_react31.createElement)( - "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( ++ "div", + { 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.")) ++ "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")), @@ -19222,7 +19914,10 @@ diff --git a/client.js b/client.js + "div", + { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, + (0, import_react31.createElement)( -+ "span", + "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)(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)), @@ -20710,7 +21405,7 @@ diff --git a/client.js b/client.js + key: `${item.from.id}>${item.to.id}`, + back: true, + d: `M ${x1} ${y1} L ${x1} ${yA} L ${lane} ${yA} L ${lane} ${yB} L ${x2} ${yB} L ${x2} ${y2}` - }); ++ }); + taken.push({ x: lane, yA: Math.min(yA, yB), yB: Math.max(yA, yB) }); + }); + const centres = []; @@ -20736,7 +21431,7 @@ diff --git a/client.js b/client.js + key: `${item.from.id}>${item.to.id}`, + back: false, + d: straight ? `M ${x1} ${y1} L ${x1} ${y2}` : `M ${x1} ${y1} L ${x1} ${yA} L ${lane} ${yA} L ${lane} ${yB} L ${x2} ${yB} L ${x2} ${y2}` -+ }); + }); + } + return routes; +} @@ -20942,9 +21637,24 @@ diff --git a/client.js b/client.js - { 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", + "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` } }, + // P-395: every wire says which two cards it joins, and carries a 10px transparent + // hit path of its own: two wires that end at the same port are one stroke to the @@ -21008,7 +21718,50 @@ diff --git a/client.js b/client.js + }, (0, import_react37.createElement)("title", null, label)) + ]; + })) -+ ), + ), +- 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)) + frames.map((frame) => (0, import_react37.createElement)("div", { + key: frame.key, + className: "bt-wf-par", @@ -21063,7 +21816,7 @@ diff --git a/client.js b/client.js + ); + }) + ) - ) ++ ) + ) + ); +} @@ -21082,13 +21835,7 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react37.createElement)("span", null, item.name), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") - ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length }) }, -- members.map((member) => { -- const leads = room2.admin === member.id; -- return (0, import_react23.createElement)( ++ ), + source === "own" && editable ? (0, import_react37.createElement)( + "span", + { className: "bt-set-actions" }, @@ -21149,58 +21896,7 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("h3", null, t("Factory presets")), + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")), + (0, import_react37.createElement)( - "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null -- ); -- }), -- adding ? [ -- ...outside.map((bot) => (0, import_react23.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ add: [bot.id] }); -- } }, -- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host checks for two members on every change, even a rename. -- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") -- )) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), -- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Manage") }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -- ), -- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) ++ "div", + { className: "bt-set-card" }, + templates.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => { + const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id); @@ -22571,18 +23267,15 @@ diff --git a/client.js b/client.js + } }, 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)( ++ "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", @@ -22615,32 +23308,38 @@ diff --git a/client.js b/client.js + })) : (0, import_react39.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react39.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react39.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ "div", ++ { className: "bt-connector-form-row" }, + (0, import_react39.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, + 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(""); ++ 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")) -+ ) + ) +- ), +- !on && form ? (0, import_react25.createElement)( + ) : null, + on && gateway && picking ? (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form" }, + "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)(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" }, + "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_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); @@ -23669,22 +24368,22 @@ diff --git a/client.js b/client.js - const groups = [ + const locked = typeof roster?.readOnly === "string" && roster.readOnly.trim() !== ""; + const groups = bot?.remote ? [ - [ -- 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, ++ [ ++ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] ++ ], ++ [ + [t("Bot settings"), GearIcon, () => actions.openSettings("bots", bot.id)] + ], + pluginItems(structure, bot, actions, roster).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 })], + isTile || locked ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })], -+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)] -+ ], -+ [ + [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, @@ -23820,11 +24519,10 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11034,248 +11034,12 @@ function migrateStorage(storage = globalThis.localStorage) { - if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { +@@ -11035,247 +11035,11 @@ function migrateStorage(storage = globalThis.localStorage) { storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); } -- } catch { + } catch { - } -} - @@ -23993,8 +24691,7 @@ diff --git a/client.js b/client.js - (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 })) - ); -+ } 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 = () => { @@ -24017,6 +24714,7 @@ diff --git a/client.js b/client.js - 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} @@ -24103,7 +24801,53 @@ diff --git a/client.js b/client.js key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) { +@@ -11320,45 +11320,88 @@ function PermissionRow({ bot, roster, actions, run }) { + ) + ); + } ++function ReadOnlyOverview({ bot, roster }) { ++ const shown = modelOf(bot, roster); ++ const models = roster.models ?? []; ++ const catalog = roster.permissions; ++ const preset = catalog?.options?.find((option) => option.value === (bot.permission ?? catalog?.defaultPreset)); ++ const folder = bot.workDir ?? ""; ++ return (0, import_react44.createElement)( ++ import_react44.Fragment, ++ null, ++ (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)( ++ "div", ++ { className: "bt-model-copy bt-readonly-value", title: shown?.ref }, ++ (0, import_react44.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Not set")), ++ shown ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? shown.provider : t("{provider} · not available", { provider: shown.provider })) : null ++ ) ++ ), ++ preset ? (0, import_react44.createElement)( ++ "div", ++ { key: "permission", className: "bt-drawer-model" }, ++ (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Permissions")), ++ (0, import_react44.createElement)( ++ "div", ++ { className: "bt-model-copy bt-readonly-value" }, ++ (0, import_react44.createElement)("span", { className: "bt-model-name" }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name) ++ ) ++ ) : null, ++ (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)( ++ "div", ++ { className: "bt-model-copy bt-readonly-value", title: folder || void 0 }, ++ (0, import_react44.createElement)("span", { className: "bt-model-name" }, folder ? folderName(folder) : t("Team workspace")), ++ folder ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, folder) : null ++ ) ++ ) ++ ); ++} function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -24531,7 +25275,7 @@ diff --git a/client.js b/client.js className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,98 +11535,104 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11535,98 +11535,106 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setEditing(false); } } @@ -24571,9 +25315,11 @@ diff --git a/client.js b/client.js + ] : null, + bot && !bot.remote && tab === "details" ? [ + // P-542/P-487: the overview's own model, permission and folder entries are writes too, and a -+ // locked window refuses every one of them. Say so once at the top and draw the read half only; -+ // the name above is already text, and the settings gear still opens the page that explains it. ++ // locked window refuses every one of them. Say so once at the top and draw the read half only, ++ // which is the values themselves (P-547) — the name above is already text, and the settings ++ // gear still opens the page that explains the window. + lockedWindow(roster) ? (0, import_react44.createElement)("div", { key: "read-only", className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, ++ lockedWindow(roster) ? (0, import_react44.createElement)(ReadOnlyOverview, { key: "read", bot, roster }) : null, + lockedWindow(roster) ? null : (0, import_react44.createElement)( "div", { key: "model", className: "bt-drawer-model" }, diff --git a/patches/ds-bot/0012-room-membership.patch b/patches/ds-bot/0012-room-membership.patch index c167cebb2..c015dd4a4 100644 --- a/patches/ds-bot/0012-room-membership.patch +++ b/patches/ds-bot/0012-room-membership.patch @@ -124,7 +124,8 @@ diff --git a/src/client/details-drawer.js b/src/client/details-drawer.js -import { GroupSettingsForm, ROOM_MODES, roomSubtitle, shownMode } from './group-settings.js' +import { FaceLead, FacePersonRow, GroupSettingsForm, ROOM_MODES, facePeople, roomSubtitle, shownMode } from './group-settings.js' import { ModelMenu, modelOf } from './model-menu.js' - import { FolderMenu } from './folder-menu.js' +-import { FolderMenu } from './folder-menu.js' ++import { FolderMenu, folderName } from './folder-menu.js' import { MemoryRow } from './memory-panel.js' -import { ScheduleSummary } from './schedules.js' +import { RoomSchedules, ScheduleSummary, lockedWindow } from './schedules.js' @@ -150,6 +151,42 @@ diff --git a/src/client/details-drawer.js b/src/client/details-drawer.js const TAB_LABELS = { details: 'Details', browser: 'Browser' } +@@ -62,6 +62,35 @@ function PermissionRow({ bot, roster, actions, run }) { + }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name)))) + } + ++// P-547: a locked window refuses the overview's writes, not its reading. The model, permission and ++// folder cards kept their writer's form or vanished whole, so their saved values went with the ++// controls; here each keeps its title and its value as plain text — the shape the name above ++// already has (#2391: the value stays, the control goes) — and no menu, preset or button is drawn. ++function ReadOnlyOverview({ bot, roster }) { ++ const shown = modelOf(bot, roster) ++ const models = roster.models ?? [] ++ const catalog = roster.permissions ++ const preset = catalog?.options?.find(option => option.value === (bot.permission ?? catalog?.defaultPreset)) ++ const folder = bot.workDir ?? '' ++ return h(Fragment, null, ++ h('div', { key: 'model', className: 'bt-drawer-model' }, ++ h('div', { className: 'bt-section-title' }, t('Model')), ++ h('div', { className: 'bt-model-copy bt-readonly-value', title: shown?.ref }, ++ h('span', { className: 'bt-model-name' }, shown?.name ?? t('Not set')), ++ shown ? h('span', { className: 'bt-model-provider' }, shown.served || models.length === 0 ++ ? shown.provider ++ : t('{provider} · not available', { provider: shown.provider })) : null)), ++ preset ? h('div', { key: 'permission', className: 'bt-drawer-model' }, ++ h('div', { className: 'bt-section-title' }, t('Permissions')), ++ h('div', { className: 'bt-model-copy bt-readonly-value' }, ++ h('span', { className: 'bt-model-name' }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name))) : null, ++ h('div', { key: 'folder', className: 'bt-drawer-model' }, ++ h('div', { className: 'bt-section-title' }, t('Working folder')), ++ h('div', { className: 'bt-model-copy bt-readonly-value', title: folder || undefined }, ++ h('span', { className: 'bt-model-name' }, folder ? folderName(folder) : t('Team workspace')), ++ folder ? h('span', { className: 'bt-model-provider' }, folder) : null))) ++} ++ + function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { + const state = useBrowserTabs(map => map[bot.id] ?? EMPTY_TABS) + const pages = useBrowserPages(map => map) @@ -113,16 +113,127 @@ function About({ text, title = 'About' }) { // A group's overview: what members read, who is in it, and who answers. Changes are @@ -372,7 +409,7 @@ diff --git a/src/client/details-drawer.js b/src/client/details-drawer.js hasStrip ? null : h('button', { type: 'button', className: 'bt-round', 'aria-label': t('Close details'), onClick: close }, h(CloseIcon))) if (!bot && !room) { return [h(Fragment, { key: 'top' }, top), -@@ -213,28 +213,50 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro +@@ -213,28 +213,52 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro h('div', { key: 'id', className: 'bt-drawer-id', 'data-move': side }, bot ? h(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== undefined), main: isMainBot, badge: false, gaze: true, pokeable: true }) : h(RoomAvatar, { room, roster, size: 64 }), @@ -424,9 +461,11 @@ diff --git a/src/client/details-drawer.js b/src/client/details-drawer.js + ] : null, + bot && !bot.remote && tab === 'details' ? [ + // P-542/P-487: the overview's own model, permission and folder entries are writes too, and a -+ // locked window refuses every one of them. Say so once at the top and draw the read half only; -+ // the name above is already text, and the settings gear still opens the page that explains it. ++ // locked window refuses every one of them. Say so once at the top and draw the read half only, ++ // which is the values themselves (P-547) — the name above is already text, and the settings ++ // gear still opens the page that explains the window. + lockedWindow(roster) ? h('div', { key: 'read-only', className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null, ++ lockedWindow(roster) ? h(ReadOnlyOverview, { key: 'read', bot, roster }) : null, + lockedWindow(roster) ? null : h('div', { key: 'model', className: 'bt-drawer-model' }, h('div', { className: 'bt-section-title' }, t('Model')), h(ModelMenu, { bot, roster, actions, run, wide: true }), diff --git a/patches/ds-bot/files/test/details-overview-readonly.test.js b/patches/ds-bot/files/test/details-overview-readonly.test.js index 422f3eb6d..70ebd07dd 100644 --- a/patches/ds-bot/files/test/details-overview-readonly.test.js +++ b/patches/ds-bot/files/test/details-overview-readonly.test.js @@ -200,3 +200,52 @@ test('P-544: a read-only group overview says why once and draws the name as plai assert.deepEqual(modelTriggers(entries), [], 'a group never had a model menu') assert.deepEqual(permissionRows(entries), [], 'a group never had permission presets') }) + +// P-547: the same locked window dropped the overview's Model, Permissions and Working folder +// cards whole, so their values went with the controls even though the name above is already +// "value kept, control removed" (`details-drawer.js`'.bt-drawer-name' as plain text). Product's +// probe on a locked window read `model name drawn = false`, `permission label = false`, +// `workDir = false`; in a writable window all three were true. A locked window refuses the +// writes, not the reading, so the three values stay on the page as text and none of the three +// controls is drawn. +const withModel = { ...bot, model: 'deepseek/deepseek-v4.1-flash', workDir: '/home/xuke/flybot' } +const modelsCatalog = [{ ref: 'deepseek/deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash', providerName: 'DeepSeek' }] +const rosterModelWith = (readOnly) => ({ + ...base, bots: [withModel], byId: { a: withModel }, models: modelsCatalog, + ...(readOnly === undefined ? {} : { readOnly }), +}) +const readOnlyValues = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-readonly-value')) +// The value line itself, without the note under it (the provider, the full path). +const readOnlyNames = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-model-name')).map(({ node }) => ownText(node).trim()) +const sectionTitles = (entries) => nodes(entries).filter(node => hasClass(node, 'bt-section-title')).map(node => ownText(node).trim()) + +test('P-547: a locked overview keeps the model, permission and folder values and draws no control', () => { + slots = [] + const entries = tree(rosterModelWith(SENTENCE)) + assert.equal(statusLine(entries).length, 1, "the host's sentence is still drawn once") + assert.equal(readOnlyValues(entries).length, 3, 'the three values are drawn, not dropped with their controls') + const names = readOnlyNames(entries) + assert.equal(names[0], 'DeepSeek V4.1 Flash', 'the model name is on the page') + assert.equal(names[1], 'Workspace', 'the permission preset is on the page, by its label') + assert.equal(names[2], 'flybot', 'the working folder is on the page by its last segment') + const titles = sectionTitles(entries) + for (const title of ['Model', 'Permissions', 'Working folder']) assert.ok(titles.includes(title), `${title} keeps its section`) + const folder = readOnlyValues(entries)[2].node + assert.equal(folder.props.title, '/home/xuke/flybot', 'the full folder path is kept as the value title') + assert.deepEqual(modelTriggers(entries), [], 'no model menu the host would refuse') + assert.deepEqual(permissionRows(entries), [], 'no permission preset the host would refuse') + assert.deepEqual(folderTriggers(entries), [], 'no working-folder menu the host would refuse') + assert.deepEqual(nodes(entries).filter(node => node.props?.['aria-haspopup'] === 'menu'), [], + 'no anchored menu of any kind is drawn in a locked overview') +}) + +test('P-547: a writable overview keeps its controls and draws no read-only value', () => { + slots = [] + const entries = tree(rosterModelWith(undefined)) + assert.deepEqual(readOnlyValues(entries), [], 'the read-only values are a locked window only') + assert.equal(modelTriggers(entries).length, 1, 'the model menu is still the writable control') + assert.equal(permissionRows(entries).length, 1, 'the permission presets are still the writable control') + assert.equal(folderTriggers(entries).length, 1, 'the working-folder menu is still the writable control') + assert.equal(ownText(modelTriggers(entries)[0].node).includes('DeepSeek V4.1 Flash'), true, + 'the writable trigger still shows the same model name') +})