diff --git a/changelog.d/1626-activity-row-running-host.fixed.md b/changelog.d/1626-activity-row-running-host.fixed.md
new file mode 100644
index 000000000..cacb22f80
--- /dev/null
+++ b/changelog.d/1626-activity-row-running-host.fixed.md
@@ -0,0 +1 @@
+After a message is sent on DeepSeek Harness 0.2.1, the Bot avatar and shimmering status line under the transcript show again: the activity row finds `RunningStatus` even when it sits inside the `conversation.chat.flow` slot ([#1626](https://github.com/ThinkFlowLab/FlyBot/issues/1626)).
diff --git a/core/client.js b/core/client.js
index d45c03aa5..8e3df0021 100644
--- a/core/client.js
+++ b/core/client.js
@@ -7115,6 +7115,16 @@ function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" };
}
+// src/client/activity-row-host.js
+var RUNNING_HOST = "[data-chat-flow] [data-chat-running]";
+var TAIL_HOST = "tail";
+function findRunningHostIn(root) {
+ if (!root || typeof root.querySelector !== "function") return null;
+ if (typeof root.matches === "function" && root.matches("[data-chat-running]")) return root;
+ if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]");
+ return root.querySelector(RUNNING_HOST);
+}
+
// src/client/activity-row.js
var import_react10 = require("react");
var import_react_dom2 = require("react-dom");
@@ -7126,8 +7136,6 @@ function elapsedLabel(ms) {
const hours = Math.floor(minutes / 60);
return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
}
-var RUNNING_HOST = "[data-chat-flow] > [data-chat-running]";
-var TAIL_HOST = "tail";
var REMOTE_GRACE_MS = 1500;
function useAwaitingReply(roster, sessionId) {
const at = roster?.remoteAwaiting?.[sessionId];
@@ -7141,10 +7149,10 @@ function useAwaitingReply(roster, sessionId) {
return shown;
}
function findHost(where) {
- if (where !== TAIL_HOST) return document.querySelector(where);
+ if (where !== TAIL_HOST) return findRunningHostIn(document);
const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]");
if (!flow) return null;
- const running = flow.querySelector(":scope > [data-chat-running]");
+ const running = findRunningHostIn(flow);
if (running) return running;
const tails = flow.querySelectorAll(":scope > * [data-turn-tail], :scope > [data-turn-tail]");
return tails.length ? tails[tails.length - 1] : null;
diff --git a/core/src/client/activity-row-host.js b/core/src/client/activity-row-host.js
new file mode 100644
index 000000000..ad6f25e6e
--- /dev/null
+++ b/core/src/client/activity-row-host.js
@@ -0,0 +1,17 @@
+// Pure host helpers for the activity row (#1626). Kept free of React so unit tests
+// import them without a DOM renderer.
+
+// DSH 0.2.0 put RunningStatus as a direct child of [data-chat-flow]. DSH 0.2.1+
+// appends it inside the conversation.chat.flow slot, so a direct-child selector
+// misses the host and the Bot avatar + shimmer never portals (#1626).
+export const RUNNING_HOST = '[data-chat-flow] [data-chat-running]'
+export const TAIL_HOST = 'tail'
+
+/** Resolve the shell running-status node under a document or flow root (0.2.0 or 0.2.1+). */
+export function findRunningHostIn(root) {
+ if (!root || typeof root.querySelector !== 'function') return null
+ if (typeof root.matches === 'function' && root.matches('[data-chat-running]')) return root
+ // An Element (the flow column) scopes to descendants; a Document uses the full path.
+ if (typeof Element !== 'undefined' && root instanceof Element) return root.querySelector('[data-chat-running]')
+ return root.querySelector(RUNNING_HOST)
+}
diff --git a/core/src/client/activity-row.js b/core/src/client/activity-row.js
index 605322f5e..824ca90c1 100644
--- a/core/src/client/activity-row.js
+++ b/core/src/client/activity-row.js
@@ -6,6 +6,9 @@ import { reducedMotion } from './motion.js'
import { t } from './i18n.js'
import { AWAIT_REPLY_MS, awaitingReply } from './awaiting.js'
import { remoteApprovalPhrase } from './remote-approval-wait.js'
+import { RUNNING_HOST, TAIL_HOST, findRunningHostIn } from './activity-row-host.js'
+
+export { RUNNING_HOST, TAIL_HOST, findRunningHostIn } from './activity-row-host.js'
// -------------------------------------------------------------------------
// The activity row: the Bot's mark and a shimmering phrase at the transcript
@@ -24,8 +27,6 @@ export { reducedMotion }
// Where the row draws: the shell's running status while a turn runs; between turns of a
// friend's chat (#443), the tail of the transcript's last turn, so it stays below the last message.
-export const RUNNING_HOST = '[data-chat-flow] > [data-chat-running]'
-export const TAIL_HOST = 'tail'
const REMOTE_GRACE_MS = 1500
function useAwaitingReply(roster, sessionId) {
const at = roster?.remoteAwaiting?.[sessionId]
@@ -39,10 +40,10 @@ function useAwaitingReply(roster, sessionId) {
return shown
}
function findHost(where) {
- if (where !== TAIL_HOST) return document.querySelector(where)
+ if (where !== TAIL_HOST) return findRunningHostIn(document)
const flow = document.querySelector('[data-conversation-scroll] [data-chat-flow]') ?? document.querySelector('[data-chat-flow]')
if (!flow) return null
- const running = flow.querySelector(':scope > [data-chat-running]')
+ const running = findRunningHostIn(flow)
if (running) return running
const tails = flow.querySelectorAll(':scope > * [data-turn-tail], :scope > [data-turn-tail]')
return tails.length ? tails[tails.length - 1] : null
diff --git a/core/test/activity-row-host.test.js b/core/test/activity-row-host.test.js
new file mode 100644
index 000000000..6bda40565
--- /dev/null
+++ b/core/test/activity-row-host.test.js
@@ -0,0 +1,46 @@
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { join } from 'node:path'
+import { readFileSync } from 'node:fs'
+import { test } from 'node:test'
+import { findRunningHostIn, RUNNING_HOST } from '../src/client/activity-row-host.js'
+
+// jsdom comes from the DSH harness (test/harness-resolve.mjs sets DSH_HARNESS).
+const { JSDOM } = createRequire(join(process.env.DSH_HARNESS, 'package.json'))('jsdom')
+
+test('RUNNING_HOST is a descendant selector so DSH 0.2.1 nested RunningStatus still matches (#1626)', () => {
+ assert.equal(RUNNING_HOST, '[data-chat-flow] [data-chat-running]')
+ assert.equal(RUNNING_HOST.includes('>'), false, 'must not require a direct child of the flow column')
+ const src = readFileSync(new URL('../src/client/activity-row.js', import.meta.url), 'utf8')
+ assert.match(src, /activity-row-host\.js/)
+ assert.doesNotMatch(src, /\[data-chat-flow\] > \[data-chat-running\]/)
+ const hostSrc = readFileSync(new URL('../src/client/activity-row-host.js', import.meta.url), 'utf8')
+ assert.match(hostSrc, /conversation\.chat\.flow/)
+})
+
+test('findRunningHostIn finds RunningStatus as a direct child (DSH 0.2.0 layout)', () => {
+ const { document } = new JSDOM(`
`).window
+ const host = findRunningHostIn(document)
+ assert.equal(host?.id, 'run')
+ assert.equal(findRunningHostIn(document.querySelector('[data-chat-flow]'))?.id, 'run')
+})
+
+test('findRunningHostIn finds RunningStatus nested under conversation.chat.flow (DSH 0.2.1+)', () => {
+ const { document } = new JSDOM(``).window
+ assert.equal(document.querySelector('[data-chat-flow] > [data-chat-running]'), null, 'direct-child selector misses 0.2.1')
+ assert.equal(findRunningHostIn(document)?.id, 'nested-run')
+ assert.equal(findRunningHostIn(document.querySelector('[data-chat-flow]'))?.id, 'nested-run')
+})
+
+test('findRunningHostIn returns null when the running host is absent', () => {
+ const { document } = new JSDOM(``).window
+ assert.equal(findRunningHostIn(document), null)
+ assert.equal(findRunningHostIn(null), null)
+})
diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md
index 2fc0e597b..f436d8f44 100644
--- a/docs/subsystems/core-client.md
+++ b/docs/subsystems/core-client.md
@@ -106,7 +106,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
- In a face-to-face room the bubble matches a mention by every alias the room host routes on (`faceAliases` in `shared/remote-label.js`, the same list `face-mentions.js` uses), so `@SelfA's Chief` typed on B is A's own Chief on A. The chip shows `@` and the seat label (`faceSeatLabel`, `SelfA · Chief`) on both homes, a bare name means the writer's own Bot (my line tries my seats first, a friend's line the friend's, whatever order the room lists its seats in, as `faceCandidates` does), a friend's Bot outside the room is not matched, and one space typed after a picked `@Name ` (the menu inserts its own) is dropped from the drawing (#305).
- A remote Bot's generated look is seeded by its handle (`remoteSeed` in `src/client/characters.js`), never by its session id. When Bot Social sends `avatar` / `color` / `avatar_updated_at`, `flybot.remote.publish` stores them on the remote-chat record (cached by `avatar_updated_at`). `lookBotForRemote` overlays a person payload's look (face-room `members[]` is the source for room avatars). A stored `color`/`avatar` still wins over the seed; missing look stays handle-seeded (#137). The @ menu icon (`menuIcons`) and the user-bubble mention chip (`withMentionLook` in `user-message.js`) draw that same record through `markBotForMention`. A stand-in that keeps the look only on `remote` is not drawn: `lookOf` does not read it, and the handle seed (a green moon for `@chief.selfa1025.home`) was showing beside the stored blue whale (#1088). The seed remains the fallback when neither the chat nor the face-room seat has a look. A friend's file-chip anchor uses `lookAvoidingLocals`, the same decision as `BotMark`; a local Bot still owns a shared `@Name` chip (#963).
- A shared display name does not restyle a local Bot (#963). The `@Name` file chip (`mentionCss` in `src/client/mention-style.js`) uses the local Bot's look when a friend has the same name; that friend's own mention anchor keeps the friend's color, and a name only a friend has keeps that friend's look. `indexRoster` does not let a remote chat replace a local Bot's id or session keys, and `rosterBot` resolves a member id to the local Bot. `@Name` in a bubble (`linkMentions`) names the local Bot when both are listed. A group reply that is not remote resolves through `rosterBot` before the index. The friend's own row keeps the friend's look, unless that drawn look is the same picture as the local Bot of the same display name, in which case the row steps its shape and the local Bot stays as stored (#1069).
-- A friend's 1:1 chat (#443): `AssistantCell` gives the reply bubbles a `bt-lead` `BotMark` from the chat record (`friendLead`; none on a `Not sent:` note). The activity row (`activity-row.js`) stays up while `awaitingReply` (`awaiting.js`, `roster.remoteAwaiting`, 10 min cap) holds. Between the send turn and the echo turn it sits in the last turn's tail, with a 1.5 s grace and `data-carry` so there is no second entrance, which keeps one row from send to reply. The social `PeerThinking` line is for face rooms only. While the row is up in a friend chat and no local tool is the newest activity, its phrase is **Waiting for their approval** (等对方批准) instead of the thinking pool (`Sorting out the details` and the rest): that wait is the other home holding the reply, and this home cannot see the card (#1087). A local tool line still wins. A friend chat shows the row for a run that started from my submission and, between runs, while `awaitingReply` holds. The friend's reply run (the echo, which has no submission) hides it at once, and only the end of my send run gets the grace. The turn-tail hiding rules in `styles.js` spare `.bt-typing` and its tail item, or the row between runs would be drawn with `display:none`.
+- A friend's 1:1 chat (#443): `AssistantCell` gives the reply bubbles a `bt-lead` `BotMark` from the chat record (`friendLead`; none on a `Not sent:` note). The activity row (`activity-row.js`; host `[data-chat-flow] [data-chat-running]`, nested under `conversation.chat.flow` on DSH 0.2.1+ — #1626) stays up while `awaitingReply` (`awaiting.js`, `roster.remoteAwaiting`, 10 min cap) holds. Between the send turn and the echo turn it sits in the last turn's tail, with a 1.5 s grace and `data-carry` so there is no second entrance, which keeps one row from send to reply. The social `PeerThinking` line is for face rooms only. While the row is up in a friend chat and no local tool is the newest activity, its phrase is **Waiting for their approval** (等对方批准) instead of the thinking pool (`Sorting out the details` and the rest): that wait is the other home holding the reply, and this home cannot see the card (#1087). A local tool line still wins. A friend chat shows the row for a run that started from my submission and, between runs, while `awaitingReply` holds. The friend's reply run (the echo, which has no submission) hides it at once, and only the end of my send run gets the grace. The turn-tail hiding rules in `styles.js` spare `.bt-typing` and its tail item, or the row between runs would be drawn with `display:none`.
- A friend's Bot the owner hid, a remote chat marked `gone`/`hidden`, and a home-level handle (`@friend.home`, not `@bot.friend.home`) must not reappear in the sidebar, the **发给 / 和谁聊…** picker, `@` mentions, or `list_bots` friends lines (`isListedRemoteChat`). `flybot.remote.publish` skips those handles so remote-chat sync marks the persisted session gone/hidden instead of opening it again; history is kept. Handle comparison is case-insensitive (`normRemoteHandle`).
- 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.
diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch
index d12c44b61..e5a1229cb 100644
--- a/patches/ds-bot/0003-flybot-api-client.patch
+++ b/patches/ds-bot/0003-flybot-api-client.patch
@@ -3,24 +3,25 @@ FlyBot patch 0003-flybot-api-client on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). G
diff --git a/src/client/activity-row.js b/src/client/activity-row.js
--- a/src/client/activity-row.js
+++ b/src/client/activity-row.js
-@@ -4,6 +4,8 @@ import { BotMark, RoomAvatar } from './mark.js'
+@@ -4,6 +4,11 @@ import { BotMark, RoomAvatar } from './mark.js'
import { Shimmer, PHRASES } from './cells.js'
import { reducedMotion } from './motion.js'
import { t } from './i18n.js'
+import { AWAIT_REPLY_MS, awaitingReply } from './awaiting.js'
+import { remoteApprovalPhrase } from './remote-approval-wait.js'
++import { RUNNING_HOST, TAIL_HOST, findRunningHostIn } from './activity-row-host.js'
++
++export { RUNNING_HOST, TAIL_HOST, findRunningHostIn } from './activity-row-host.js'
// -------------------------------------------------------------------------
// The activity row: the Bot's mark and a shimmering phrase at the transcript
-@@ -20,21 +20,76 @@ export function elapsedLabel(ms) {
+@@ -20,21 +20,74 @@ export function elapsedLabel(ms) {
}
export { reducedMotion }
-function useRunningHost(active) {
+// Where the row draws: the shell's running status while a turn runs; between turns of a
+// friend's chat (#443), the tail of the transcript's last turn, so it stays below the last message.
-+export const RUNNING_HOST = '[data-chat-flow] > [data-chat-running]'
-+export const TAIL_HOST = 'tail'
+const REMOTE_GRACE_MS = 1500
+function useAwaitingReply(roster, sessionId) {
+ const at = roster?.remoteAwaiting?.[sessionId]
@@ -34,10 +35,10 @@ diff --git a/src/client/activity-row.js b/src/client/activity-row.js
+ return shown
+}
+function findHost(where) {
-+ if (where !== TAIL_HOST) return document.querySelector(where)
++ if (where !== TAIL_HOST) return findRunningHostIn(document)
+ const flow = document.querySelector('[data-conversation-scroll] [data-chat-flow]') ?? document.querySelector('[data-chat-flow]')
+ if (!flow) return null
-+ const running = flow.querySelector(':scope > [data-chat-running]')
++ const running = findRunningHostIn(flow)
+ if (running) return running
+ const tails = flow.querySelectorAll(':scope > * [data-turn-tail], :scope > [data-turn-tail]')
+ return tails.length ? tails[tails.length - 1] : null
diff --git a/patches/ds-bot/0007-version-and-tooling.patch b/patches/ds-bot/0007-version-and-tooling.patch
index a4745c7ef..9de888e6a 100644
--- a/patches/ds-bot/0007-version-and-tooling.patch
+++ b/patches/ds-bot/0007-version-and-tooling.patch
@@ -1,5 +1,17 @@
FlyBot patch 0007-version-and-tooling on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/).
+diff --git a/dev/harness/package.json b/dev/harness/package.json
+--- a/dev/harness/package.json
++++ b/dev/harness/package.json
+@@ -3,7 +3,7 @@
+ "private": true,
+ "description": "The DeepSeek Harness packages that the unit tests and the linter run against. Install with npm run harness in ds-bot/.",
+ "dependencies": {
+- "@deepseek-ai/dsh": "0.2.0-rc.2",
++ "@deepseek-ai/dsh": "0.2.1-rc.2",
+ "jsdom": "29.1.1",
+ "oxlint": "1.76.0"
+ }
diff --git a/dev/smoke.mjs b/dev/smoke.mjs
--- a/dev/smoke.mjs
+++ b/dev/smoke.mjs
@@ -871,17 +883,3 @@ diff --git a/test/diagnostics.test.js b/test/diagnostics.test.js
+test('Report on GitHub opens a new issue in FlyBot\'s own repository', () => {
+ assert.equal(ISSUES_URL, 'https://github.com/ThinkFlowLab/FlyBot/issues/new')
+})
-
-diff --git a/dev/harness/package.json b/dev/harness/package.json
---- a/dev/harness/package.json
-+++ b/dev/harness/package.json
-@@ -2,8 +2,8 @@
- "name": "ds-bot-test-harness",
- "private": true,
- "description": "The DeepSeek Harness packages that the unit tests and the linter run against. Install with npm run harness in ds-bot/.",
- "dependencies": {
-- "@deepseek-ai/dsh": "0.2.0-rc.2",
-+ "@deepseek-ai/dsh": "0.2.1-rc.2",
- "jsdom": "29.1.1",
- "oxlint": "1.76.0"
- }
diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch
index 6d9ff837d..a1b0ca805 100644
--- a/patches/ds-bot/0008-client-bundle.patch
+++ b/patches/ds-bot/0008-client-bundle.patch
@@ -1864,7 +1864,7 @@ diff --git a/client.js b/client.js
var formatTokens = (count) => count >= 1e6 ? `${(count / 1e6).toFixed(1)}M` : count >= 1e3 ? `${(count / 1e3).toFixed(1)}K` : String(count);
function usageOf(list, ids) {
let input = 0;
-@@ -1624,900 +1624,1977 @@ function usageOf(list, ids) {
+@@ -1624,201 +1624,1174 @@ function usageOf(list, ids) {
const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0;
return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit });
}
@@ -2126,8 +2126,6 @@ diff --git a/client.js b/client.js
- animation.onfinish = () => {
- delete node.dataset.morph;
- };
-- animation.oncancel = () => {
-- if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph;
+function isAgentSession(roster, cache, sessionId) {
+ if (sessionId === void 0 || sessionId === null || sessionId === "") return false;
+ if (roster.ready) return roster.agentOf[sessionId] !== void 0;
@@ -2161,8 +2159,7 @@ diff --git a/client.js b/client.js
+ const clamp = (value) => Math.max(-1, Math.min(1, value));
+ node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2));
+ node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2));
- };
-- });
++ };
+ const onMove = (event) => {
+ point = { x: event.clientX, y: event.clientY };
+ if (!frame) frame = requestAnimationFrame(place);
@@ -2180,9 +2177,7 @@ diff --git a/client.js b/client.js
+ document.documentElement.removeEventListener("pointerleave", onLeave);
+ };
+ }, [enabled]);
- }
--function useLinger(value, ms) {
-- const [kept, setKept] = (0, import_react.useState)(value);
++}
+var FIDGETS = ["hop", "sway", "stretch", "look"];
+function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) {
+ const look = given ?? (bot?.remote ? lookAvoidingLocals(bot) : lookOf(bot));
@@ -2262,22 +2257,17 @@ diff --git a/client.js b/client.js
+function useSettled(state) {
+ const prev = (0, import_react.useRef)(state);
+ const [settled, setSettled] = (0, import_react.useState)(false);
- (0, import_react.useEffect)(() => {
-- if (value) {
-- setKept(value);
++ (0, import_react.useEffect)(() => {
+ const was = prev.current;
+ prev.current = state;
+ if (state !== "idle") {
+ setSettled(false);
- return void 0;
- }
-- const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms);
++ return void 0;
++ }
+ if (was === "idle" || was === "alert") return void 0;
+ setSettled(true);
+ const timer = setTimeout(() => setSettled(false), 1800);
- return () => clearTimeout(timer);
-- }, [value]);
-- return [value || kept, !value && Boolean(kept)];
++ return () => clearTimeout(timer);
+ }, [state]);
+ return settled;
+}
@@ -2370,108 +2360,8 @@ diff --git a/client.js b/client.js
+ return (0, import_react.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
+ })
+ );
- }
-
--// src/client/styles.js
--var SHELL_CSS = `
--body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)}
--body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)}
--${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")}
--${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")}
--.bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
--.bt-mark svg{width:100%;height:100%;overflow:visible}
--.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
--.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
--.bt-mark .bt-b{transform-origin:50% 100%}
--.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
--/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a
-- mark only for its state; normal, the default, adds petting, pokes and eyes that follow
-- the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */
--body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)}
--body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
--body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop}
--body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway}
--body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch}
--body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
--@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}}
--@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}}
--@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}}
--@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
--body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
--body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
--@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
--/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
-- size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
-- closed in a happy arc, and springs back when the pointer leaves. */
--:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%}
--.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out}
--.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in}
--.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out}
--body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite}
--body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none}
--body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s}
--@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}}
--.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out}
--.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out}
--body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none}
--body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none}
--.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite}
--.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite}
--.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite}
--.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite}
--.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite}
--.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%}
--.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite}
--.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite}
--.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite}
--.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%}
--.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite}
--/* A question for you hops twice when it arrives and the badge keeps it in view after;
-- a lively mark keeps hopping. */
--.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
--.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite}
--.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
--body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite}
--.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1}
--.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%}
--@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}}
--.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important}
--.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important}
--.bt-mark[data-poke]{cursor:pointer}
--body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important}
--body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
--@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}}
--@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}}
--@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}}
--@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}}
--@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}}
--@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}}
--@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}}
--@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}}
--@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}}
--@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}}
--@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}}
--@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}}
--@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}}
--/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */
--@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}}
--@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}}
--@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}}
--.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)}
--.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))}
--.bt-badge-admin svg{width:7px;height:7px}
--.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none}
--.bt-badge-main svg{width:100%;height:100%;overflow:visible}
--.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke}
--.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px}
--.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0}
--.bt-cluster>*{position:absolute}
--.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums}
--.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)}
--.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0}
--.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)}
--.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important}
++}
++
+// src/client/room-invite-preview.js
+var INVITE_PREVIEW_NAME_MAX = 60;
+function stripInviteMarkers(text) {
@@ -2552,42 +2442,7 @@ diff --git a/client.js b/client.js
+ const total = Math.max(1, Math.ceil(left / 1e3));
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`;
+}
-
--.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
--.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
--.bt-brand-whale{display:inline-flex;color:var(--bt-accent)}
--.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
--.bt-round:hover{background:var(--bt-hover)}
--.bt-round:active{transform:scale(.94)}
--.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
--.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
--.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
--.bt-tile:hover{background:var(--bt-hover)}
--.bt-tile[aria-current=page]{background:var(--bt-active)}
--.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
--.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
--.bt-row:hover{background:var(--bt-hover)}
--.bt-row[aria-current=page]{background:var(--bt-active)}
--.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
--.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
--.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
--.bt-unread-wait{background:var(--bt-star)}
--.bt-rail{align-items:center;padding:0 0 8px}
--.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
--.bt-side-foot{display:flex;padding-top:8px}
--.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
--.bt-connect:hover{background:var(--bt-hover)}
--.bt-connect svg{flex:none;width:15px;height:15px}
--.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}
--.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
--.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
--@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}}
++
+// src/client/approval-claims.js
+var claims = /* @__PURE__ */ new Map();
+var claimListeners = /* @__PURE__ */ new Set();
@@ -2613,56 +2468,13 @@ diff --git a/client.js b/client.js
+ claimListeners.delete(fn);
+ };
+}
-
--.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
--.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
--.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
--.bt-pill:active{transform:scale(.965)}
--.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
--.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
--.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
--.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
--.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)}
--.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms}
--.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)}
--.bt-pill-mem-ico svg{width:16px;height:16px}
--.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both}
--@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}}
--.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
--@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}}
--.bt-swap{color:var(--bt-ink-3);font-size:12px}
--.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease}
--.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none}
--.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)}
--.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)}
--@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}}
++
+// src/shared/approval-waiting.js
+var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]);
+function isApprovalWaiting(status) {
+ return WAITING_STATUSES.includes(status);
+}
-
--.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
--.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
--.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
--.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
--.bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
--.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important}
--.bt-bubble p{margin:0}
--.bt-bubble p + p{margin-top:6px}
--.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px}
--.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap}
--.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat}
--.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0}
--.bt-group-msg:first-child{margin-top:0}
--.bt-lead{flex:none;display:flex;margin-bottom:1px}
--button.bt-lead{border:0;background:none;padding:0;cursor:pointer}
--/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */
--.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px}
--.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px}
--.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
--.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
++
+// src/shared/held-retry.js
+var TERMINAL = [
+ // A friend's Bot that is gone or no longer shared (plugins/social/src/social.js: botGoneError / notSharedError).
@@ -2682,79 +2494,7 @@ diff --git a/client.js b/client.js
+ const said = `${record.error ?? ""} ${record.code ?? ""}`;
+ return !TERMINAL.some((reason) => reason.test(said));
+};
-
--.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
--body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)}
--@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}}
--.bt-q-settled{animation:none}
--.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0}
--.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
--.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere}
--.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
--.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
--.bt-q-x svg{width:14px;height:14px}
--.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
--.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px}
--.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease}
--.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)}
--.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px}
--.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none}
--button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0}
--.bt-q-raised .bt-q-opt::before{display:none}
--button.bt-q-opt:hover{background:var(--bt-hover)}
--button.bt-q-opt:active{background:var(--bt-active)}
--.bt-q-opt[aria-pressed=true]{background:var(--bt-active)}
--.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)}
--.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease}
--.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px}
--/* With a description the key spans the two text lines less 2px above and below. */
--.bt-q-opt:has(.bt-q-desc){--bt-key:34px}
--.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px}
--.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0}
--.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere}
--.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere}
--.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
--.bt-q-settled .bt-q-check{animation:none}
--.bt-q-check svg{width:16px;height:16px}
--.bt-q-settled .bt-key{opacity:.4}
--.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)}
--.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)}
--.bt-q.bt-q-settled{gap:6px;padding:12px 16px}
--.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500}
--.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0}
--.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0}
--.bt-q-settled .bt-q-opt{padding:4px 8px}
--.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box}
--.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0}
--.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease}
--.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)}
--.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere}
--.bt-q-input:not(:placeholder-shown){field-sizing:content}
--.bt-q-input::placeholder{color:var(--bt-ink-3)}
--.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}}
--.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards}
--@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}}
--.bt-q-submit:hover:not(:disabled){opacity:.86}
--.bt-q-submit:disabled{opacity:.4;cursor:default}
--.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-q-btn:hover{background:var(--bt-active)}
--.bt-q-foot{display:flex;justify-content:flex-end;gap:8px}
--.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)}
--.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px}
--.bt-q-raised .bt-q-head:last-child{padding-bottom:16px}
--.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px}
--.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)}
--.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)}
--.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)}
--.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)}
--.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)}
--.bt-q-raised .bt-q-body{padding:2px 6px 0}
--.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)}
--.bt-q-ownrow{cursor:text}
--.bt-q-ownrow .bt-q-input{color:var(--bt-ink)}
--.bt-q-raised .bt-q-foot{padding:0 8px 8px}
--@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
++
+// src/client/flybot-slots.js
+var import_react2 = require("react");
+var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
@@ -3048,158 +2788,7 @@ diff --git a/client.js b/client.js
+.bt-flybot-approval-files { margin: 0 0 8px; padding-left: 18px; font-size: 12px; }
+.bt-file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 0 30px; }
+.bt-file-chip { font-size: 12px; padding: 3px 8px; border-radius: 8px; border: 1px solid var(--bt-line, rgba(0,0,0,.15)); background: transparent; cursor: pointer; }
-
--.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
--.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
--.bt-to-label{color:var(--bt-ink-2)}
--.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
--.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px}
--.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px}
--.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
--.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
--.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%}
--.bt-option[aria-selected=true]{background:var(--bt-active)}
--.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
--.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out}
--.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
--.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
--.bt-nc-spacer{flex:1}
--.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center}
--.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box}
--.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none}
--.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box}
--.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
--.bt-nc-send:disabled{opacity:.3;cursor:default}
--.bt-create{flex:1;display:flex;flex-direction:column;min-width:0}
--.bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box}
--.bt-create-gap{width:28px}
--.bt-create-title{font-size:14px;line-height:20px;font-weight:500}
--.bt-create-body{flex:1;overflow-y:auto;padding:28px 16px 32px;display:flex;justify-content:center}
--.bt-create-card{width:100%;max-width:400px;display:flex;flex-direction:column;gap:18px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .22s ease-out both}
--.bt-create-preview{display:flex;flex-direction:column;align-items:center;gap:10px}
--.bt-create-pop{display:inline-flex;animation:bt-create-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} both}
--@keyframes bt-create-pop{from{transform:scale(.72);opacity:.4}}
--.bt-create-name{font-size:17px;line-height:24px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-create-step{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-bottom:-10px}
--.bt-create-looks{display:flex;flex-direction:column;gap:10px;padding:10px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)}
--.bt-create-lookbar{justify-content:center}
--.bt-create-foot{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}
--.bt-create-go{height:36px;padding:0 20px}
--@media (prefers-reduced-motion:reduce){.bt-create-card,.bt-create-pop{animation:none}}
--.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
--.bt-send:disabled{opacity:.35;cursor:default}
--.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
--
--.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0}
--.bt-dialog .bt-head{height:62px}
--.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box}
--.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px}
--.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both}
--.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none}
--@keyframes bt-sheet-in{from{transform:translateY(24px)}}
--@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}}
--.bt-exchange .bt-dialog{position:relative}
--.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin}
--.bt-exchange .bt-tools{display:none}
--.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both}
--.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none}
--.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards}
--@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}}
--@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}}
--@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}}
--.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer}
--.bt-soft:hover{background:var(--bt-active)}
--.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0}
--
--.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none}
--.bt-panel[data-instant]{transition:none}
--@keyframes bt-pane-in{from{transform:translateX(100%)}}
--[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none}
--.bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)}
--.bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize}
--.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column}
--.bt-drawer-top{display:flex;justify-content:flex-end;gap:8px;padding:12px 17px 6px 12px}
--.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
--.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
--.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-drawer-name:hover{background:var(--bt-hover)}
--.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
--.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
--.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0}
--.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap}
--.bt-tab:hover{color:var(--bt-ink)}
--.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)}
--.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent}
--.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px}
--.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px}
--.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px}
--.bt-muted{color:var(--bt-ink-2)}
--.bt-swatches{display:flex;flex-wrap:wrap;gap:6px}
--.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)}
--.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)}
--.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical}
--.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
--.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)}
--.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums}
--.bt-danger{color:#e02135}
--
--/* Bot settings: fields, the avatar editor popover, and the details-pane sub-view. */
--.bt-field{display:flex;flex-direction:column;gap:6px}
--.bt-field-label{font-size:13px;line-height:18px;font-weight:500}
--.bt-input{width:100%;box-sizing:border-box;height:36px;border:1px solid var(--bt-line);border-radius:8px;padding:0 10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;outline:none}
--.bt-input:focus{border-color:var(--bt-line-2)}
--.bt-input-area{height:auto;min-height:96px;padding:8px 10px;line-height:18px;resize:vertical}
--.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-3)}
--.bt-soft-sm{height:28px;padding:0 12px;font-size:12px}
--.bt-set-avatar{display:flex;justify-content:center;padding:4px 0}
--.bt-avatar-anchor{position:relative}
--.bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0}
--.bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)}
--.bt-avedit{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:300px;background:var(--bt-main);border:1px solid var(--bt-line);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.18);padding:10px;z-index:70;display:flex;flex-direction:column;gap:10px;color:var(--bt-ink);transform-origin:top center;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
--.bt-bs .bt-avedit{transform-origin:32px 0}
--.bt-avedit-top{display:flex;align-items:center;justify-content:space-between}
--.bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px}
--.bt-avedit-tab{position:relative;z-index:1;border:0;background:none;border-radius:999px;padding:3px 10px;font-size:12px;line-height:16px;color:var(--bt-ink-2);cursor:pointer}
--.bt-avedit-tab[aria-selected=true]{background:var(--bt-main);color:var(--bt-ink)}
--.bt-avedit-tabs>.bt-thumb{top:2px;bottom:2px;border-radius:999px;background:var(--bt-main);box-shadow:0 1px 2px rgba(0,0,0,.06)}
--.bt-avedit-tabs[data-glide] .bt-avedit-tab[aria-selected=true]{background:transparent}
--.bt-avedit-reset{color:var(--bt-ink-3)}
--.bt-avedit-reset:hover{color:var(--bt-ink)}
--.bt-shapes{display:flex;flex-direction:column;gap:2px}
--.bt-shape-row{display:grid;grid-template-columns:repeat(6,1fr);gap:2px}
--.bt-shape{position:relative;aspect-ratio:1;border:0;border-radius:8px;background:none;color:var(--bt-ink);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
--.bt-shape:hover{background:var(--bt-hover)}
--.bt-shape .bt-ring{position:absolute;left:50%;top:50%;width:36px;height:36px;translate:-50% -50%;overflow:visible;opacity:0;scale:.82;pointer-events:none;transition:opacity .14s ease,${spring(SPRING_POP, "scale")}}
--.bt-shape[aria-pressed=true] .bt-ring{opacity:1;scale:1}
--.bt-colors{display:flex;justify-content:space-between}
--.bt-color{width:22px;height:22px;border-radius:50%;border:2px solid transparent;padding:2px;background:none;cursor:pointer;box-sizing:border-box;transition:border-color .14s ease,${spring(SPRING_TAP, "scale")}}
--.bt-color[aria-pressed=true] span{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--.bt-color span{display:block;width:100%;height:100%;border-radius:50%}
--.bt-color[aria-pressed=true]{border-color:var(--bt-ink)}
--.bt-dropzone{border:1.5px dashed var(--bt-line-2);border-radius:10px;padding:18px 12px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--bt-ink-2);font-size:12px;line-height:16px;text-align:center}
--.bt-dropzone[data-over]{border-color:var(--bt-accent);color:var(--bt-ink)}
--.bt-dropzone img{width:64px;height:64px;border-radius:50%;object-fit:cover}
--.bt-set-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
--.bt-about{white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
--.bt-about[data-open]{display:block;-webkit-line-clamp:unset}
--.bt-more{border:0;background:none;color:var(--bt-ink-3);font-size:12px;line-height:16px;cursor:pointer;padding:2px 0}
--.bt-more:hover{color:var(--bt-ink)}
--.bt-subhead{display:flex;align-items:center;gap:4px;padding:10px 12px 4px}
--.bt-subhead-title{flex:1;font-size:15px;line-height:24px;font-weight:600;padding-left:4px}
--.bt-icon-btn{width:28px;height:28px;border-radius:50%;border:0;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
--.bt-icon-btn:hover{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-set-body{gap:20px}
--.bt-drawer-model{display:flex;flex-direction:column}
--.bt-drawer-model .bt-note{margin-top:6px}
--.bt-drawer-permission{display:flex;flex-direction:column}
--.bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135}
++
+.bt-flybot-section { margin-top: 8px; border-top: 1px solid var(--bt-line, rgba(0,0,0,.08)); padding-top: 6px; }
+/* A section whose component renders null leaves the wrapper empty, and an empty wrapper must not draw
+ the separator: #818's "no border" held for the component but not for the box around it. Keyed on
@@ -3228,66 +2817,7 @@ diff --git a/client.js b/client.js
+.bt-flybot-pointer-note { opacity: .65; white-space: nowrap; }
+.bt-flybot-pointer button { border: 0; border-radius: 999px; padding: 3px 10px; font-size: 12px; cursor: pointer; background: var(--bt-accent, #2563eb); color: #fff; }
+`;
-
--/* Settings dialog: a 198px page list beside the page. */
--.bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:60;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-scrim{position:absolute;inset:0;background:rgba(0,0,0,.32);animation:bt-fade .22s ease-out}
--body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)}
--.bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
--.bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))}
--:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none}
--:is(.bt-settings-layer,.bt-mem-layer)[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in}
--:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]>[role=dialog]{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards}
--@keyframes bt-dialog-rise{from{transform:translateY(12px) scale(.96)}}
--@keyframes bt-dialog-sink{to{opacity:0;transform:translateY(6px) scale(.975);filter:blur(3px)}}
--@media (prefers-reduced-motion:reduce){.bt-settings{transition:none}.bt-settings,.bt-scrim,.bt-settings-page>*{animation:bt-fade .12s ease-out!important}}
--.bt-settings-nav{position:relative;width:198px;flex:none;padding:16px 8px;border-right:.5px solid var(--bt-line);display:flex;flex-direction:column;gap:2px;background:var(--bt-sidebar);box-sizing:border-box}
--.bt-settings-item{position:relative;z-index:1;display:flex;align-items:center;gap:8px;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left;transition:background-color .12s ease,color .12s ease}
--.bt-settings-item:hover{color:var(--bt-ink);background:var(--bt-hover)}
--.bt-settings-item[aria-current=page]{background:var(--bt-active);color:var(--bt-ink)}
--.bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)}
--.bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent}
--.bt-settings-shell{margin-top:auto}
--/* Usage while another page is open: out of the flow at Usage's own width, so it keeps
-- its layout, and not rendered. */
--.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
--/* The page column takes the dialog's final size at once, so while the dialog resizes
-- only its edge moves and the page does not lay out again every frame. */
--.bt-settings-main{flex:none;display:flex;flex-direction:column;min-width:0;width:calc(var(--bt-set-w) - 198px);height:var(--bt-set-h)}
--.bt-settings-head{display:flex;align-items:center;gap:6px;padding:16px 56px 4px 20px}
--.bt-settings-head h2{margin:0;font-size:15px;line-height:24px;font-weight:600;animation:bt-veil-in .2s ease-out}
--.bt-settings-head>.bt-icon-btn{animation:bt-back-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
--@keyframes bt-back-in{from{translate:6px 0;scale:.8}}
--.bt-settings-scroll{flex:1;overflow-y:auto;padding:12px 20px 24px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin}
--.bt-settings-page>:not(.bt-us){animation:bt-page-up ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .22s ease-out backwards}
--.bt-settings-page[data-move=in]>:not(.bt-us){animation-name:bt-page-in,bt-veil-in}
--.bt-settings-page[data-move=out]>:not(.bt-us){animation-name:bt-page-out,bt-veil-in}
--.bt-settings-page>:nth-child(2){animation-delay:30ms}
--.bt-settings-page>:nth-child(3){animation-delay:60ms}
--.bt-settings-page>:nth-child(4){animation-delay:90ms}
--.bt-settings-page>:nth-child(n+5){animation-delay:120ms}
--@keyframes bt-page-up{from{transform:translateY(10px)}}
--@keyframes bt-page-in{from{transform:translateX(18px)}}
--@keyframes bt-page-out{from{transform:translateX(-18px)}}
--.bt-settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
--.bt-settings-close:hover{background:var(--bt-active);color:var(--bt-ink)}
--.bt-settings-form{display:flex;flex-direction:column;gap:16px}
--.bt-set-section h3{font-size:13px;line-height:18px;font-weight:600;margin:0 0 8px;padding:0 4px}
--.bt-set-card{border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card);display:flex;flex-direction:column}
--.bt-set-card>*+*{border-top:.5px solid var(--bt-line)}
--.bt-set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;text-align:left;width:100%;box-sizing:border-box;border-radius:0}
--button.bt-set-row{cursor:pointer}
--button.bt-set-row:hover{background:var(--bt-hover)}
--.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)}
--.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px}
--.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px}
--.bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}
--.bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))}
--.bt-set-control{flex:none;display:flex;align-items:center;gap:6px}
--.bt-set-value{font-size:13px;color:var(--bt-ink-2);text-align:right}
--.bt-set-bot{flex:1;display:flex;align-items:center;gap:8px;min-width:0}
--.bt-set-paste{flex-direction:column;align-items:stretch;gap:8px}
--.bt-chevron{color:var(--bt-ink-3);display:inline-flex;flex:none}
++
+// src/shared/failure-copy.js
+var providerCode = (text) => {
+ const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text);
@@ -3314,21 +2844,7 @@ diff --git a/client.js b/client.js
+ const withFeature = "This model does not support {feature}. Pick another model in the Bot's settings.";
+ return fill2(translate(withFeature, { feature }), { feature });
+}
-
--/* Settings → Connectors. */
--.bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px}
--.bt-connector-head{display:flex;align-items:center;gap:12px;min-width:0}
--.bt-connector-mark{flex:none;width:36px;height:36px;border-radius:10px;background:var(--bt-card);border:.5px solid var(--bt-line);display:flex;align-items:center;justify-content:center;color:var(--bt-ink)}
--.bt-connector-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
--.bt-connector-name{font-size:14px;line-height:20px;font-weight:500}
--.bt-connector-status{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-connector[data-state=ready] .bt-connector-status{color:var(--bt-ink-2)}
--.bt-connector[data-state=error] .bt-connector-status{color:#e02135;white-space:normal}
--.bt-connector-actions{flex:none;display:flex;gap:6px}
--.bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
--.bt-connector-form-row{display:flex;align-items:center;gap:6px}
--.bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)}
--.bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135}
++
+// src/shared/turn-failures.js
+var FAILURE_LINES_SHOWN = 3;
+var failureLines = (snapshot, place, t2, limit = FAILURE_LINES_SHOWN) => {
@@ -3341,15 +2857,7 @@ diff --git a/client.js b/client.js
+ ...Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {}
+ }));
+};
-
--/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the
-- shell offers them and CSS-module name suffixes elsewhere. */
--[class*="_logoRow"],[class*="_newSession"],nav[class*="_panelList"]{display:none!important}
--[class*="_footArea"]{display:flex!important;flex-direction:row-reverse;align-items:center;gap:9px;padding:8px 6px 6px!important}
--[class*="_footArea"]>[class*="_footerActions"]{flex:1 1 auto;min-width:0}
--[class*="_footArea"]>[class*="_settingsArea"]{flex:none;width:auto!important}
--[class*="_footArea"] button[aria-label="Settings"]{width:auto!important;padding:0!important;background:none!important;border-radius:50%!important;height:auto!important}
--[class*="_widthHandle"]{display:none!important}
++
+// src/client/turn-failures.js
+var import_react3 = require("react");
+function TurnFailures({ sessionId, work }) {
@@ -3379,66 +2887,7 @@ diff --git a/client.js b/client.js
+.bt-turn-failure{display:flex;gap:6px;align-items:baseline;padding:6px 10px;border-radius:8px;font-size:12px;line-height:1.45;background:color-mix(in srgb,var(--bt-ink) 6%,transparent);color:color-mix(in srgb,var(--bt-ink) 72%,transparent)}
+.bt-turn-failure-streak{opacity:.72}
+`;
-
--.bt-voice-pane{flex-direction:column;align-items:center;justify-content:center;animation:bt-fade .2s ease-out}
--.bt-voice-stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:24px;max-width:560px;text-align:center}
--.bt-voice-orb{position:relative;width:200px;height:200px;display:grid;place-items:center;margin-bottom:8px}
--.bt-voice-ring{position:absolute;inset:34px;border-radius:50%;background:var(--bt-hover);transform:scale(calc(1 + var(--bt-level,0) * .55));transition:transform .08s linear}
--.bt-voice-ring-2{inset:14px;background:none;box-shadow:inset 0 0 0 1px var(--bt-line-2);transform:scale(calc(.94 + var(--bt-level,0) * .3));opacity:.8}
--.bt-voice-stage[data-phase=thinking] .bt-voice-ring{animation:bt-voice-breathe 1.6s ease-in-out infinite}
--.bt-voice-stage[data-phase=speaking] .bt-voice-ring{animation:bt-voice-speak .9s ease-in-out infinite}
--@keyframes bt-voice-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
--@keyframes bt-voice-speak{0%,100%{transform:scale(1.04)}30%{transform:scale(1.2)}60%{transform:scale(1.1)}}
--.bt-voice-orb>.bt-mark{position:relative}
--.bt-voice-name{font-size:16px;line-height:22px;font-weight:500}
--.bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px}
--.bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
--.bt-voice-note{font-size:12px;color:#e02135}
--.bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease}
--.bt-voice-setup:hover{background:var(--bt-active)}
--.bt-voice-bar{display:flex;gap:16px;padding:0 0 32px}
--.bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease}
--.bt-voice-btn:hover{background:var(--bt-active)}
--.bt-voice-btn:active{transform:scale(.94)}
--.bt-voice-btn[aria-pressed=true]{background:var(--bt-user);color:var(--bt-user-ink)}
--.bt-voice-slash{position:absolute;width:24px;height:2px;border-radius:1px;background:currentColor;transform:rotate(-45deg)}
--.bt-voice-end{background:#ff3e51;color:#fff}
--.bt-voice-end:hover{background:#e02135}
--@keyframes bt-fade{from{opacity:0}to{opacity:1}}
--@keyframes bt-blur-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}}
--@media (prefers-reduced-motion:reduce){.bt-voice-pane,.bt-voice-caption,.bt-voice-ring{animation:none!important}}
--.bt-msg{display:flex;flex-direction:column;align-items:flex-start;max-width:min(80%,560px,calc(100% - 82px))}
--/* The bubble's row: reactions hang below it, so the hover toolbar (and a group
-- sender's avatar) line up with the bubble alone. */
--.bt-msg-line{position:relative;display:flex;align-items:flex-end;gap:8px;max-width:100%}
--.bt-msg-line>.bt-bubble{max-width:none;min-width:0}
--/* The same bubble width as beside an avatar column: 80% of (100% - 30px), plus the 30px. */
--.bt-astack[data-lead]>.bt-msg{max-width:min(calc(80% + 6px),590px,calc(100% - 52px))}
--.bt-tools{position:absolute;top:50%;left:100%;margin-left:6px;transform:translateY(-50%);display:flex;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s}
--.bt-tools::after{content:"";position:absolute;top:-8px;bottom:-8px;right:100%;width:128px;z-index:-1}
--.bt-msg:hover>.bt-msg-line>.bt-tools,.bt-msg:focus-within>.bt-msg-line>.bt-tools,.bt-tools[data-open]{opacity:1;visibility:visible}
--.bt-tool{width:24px;height:24px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease}
--.bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-tool:active{background:var(--bt-active)}
--.bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
--.bt-react-strip{position:fixed;z-index:60;pointer-events:auto;display:flex;gap:2px;padding:4px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.08),0 4px 6px -4px rgba(0,0,0,.06);transform:translate(-50%,-100%);transform-origin:bottom center;animation:bt-pop-lift ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
--.bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
--.bt-react-strip button:nth-child(2){animation-delay:20ms}
--.bt-react-strip button:nth-child(3){animation-delay:40ms}
--.bt-react-strip button:nth-child(4){animation-delay:60ms}
--.bt-react-strip button:nth-child(n+5){animation-delay:80ms}
--.bt-react-strip button:hover{background:var(--bt-hover);transform:scale(1.15)}
--.bt-react-strip button:active{transform:scale(.92)}
--.bt-reacts{display:flex;gap:4px;margin-top:4px}
--.bt-react{height:22px;min-width:30px;padding:0 6px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);font-size:13px;line-height:20px;cursor:pointer;animation:bt-react-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing};transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}}
--.bt-react:hover{background:var(--bt-hover)}
--.bt-react:active{scale:.92}
--@keyframes bt-react-pop{from{transform:scale(.6);opacity:0}}
--.bt-bubble a[href^="${MENTION_ORIGIN}"] svg{display:none}
--.bt-you{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,var(--bt-sidebar));color:var(--bt-ink-2);box-shadow:inset 0 0 0 1px var(--bt-line);transition:background-color .12s ease}
--button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))}
--.bt-you-wrap{display:inline-flex;padding:0}
--.bt-connect-foot{width:100%;height:36px}
++
+// src/client/themes.js
+var THEME_KEYS = ["bg", "sidebar", "bubble", "card", "text", "user", "userText", "accent"];
+var THEMES = {
@@ -3543,114 +2992,7 @@ diff --git a/client.js b/client.js
+ };
+}
+var allThemes = () => ({ ...THEMES, ...customThemes });
-
--.bt-menu{position:fixed;pointer-events:auto;z-index:60;min-width:200px;padding:6px;border-radius:12px;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),0 0 0 1px rgba(228,228,228,.04);display:flex;flex-direction:column;gap:2px;transform-origin:var(--bt-origin,top left);animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box}
--.bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box}
--.bt-menu button:hover{background:var(--bt-hover)}
--.bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px}
--.bt-menu .bt-danger{color:#c21d2e}
--.bt-menu-sep{height:.5px;flex:none;background:var(--bt-line-2);margin:4px 8px}
--.bt-menu-up{--bt-origin:bottom left;animation-name:bt-pop-lift,bt-veil-in}
--.bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards}
--.bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums}
--.bt-palette-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:70;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out}
--.bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
--.bt-palette-layer[data-leaving]{pointer-events:none}
--.bt-palette-layer[data-leaving] .bt-backdrop{opacity:0;transition:opacity .16s ease-in}
--.bt-palette-layer[data-leaving] .bt-palette{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards}
--@keyframes bt-palette-in{from{translate:0 -10px;scale:.96}}
--.bt-palette-head{display:flex;align-items:center;gap:4px;padding:14px 10px 14px 14px;border-bottom:.5px solid var(--bt-line-2)}
--.bt-palette-glyph{display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;color:var(--bt-ink-3);margin-right:4px}
--.bt-palette-head input{flex:1;min-width:0;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0}
--.bt-palette-head input::placeholder{color:var(--bt-ink-3)}
--.bt-palette-clear{width:18px;height:18px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
--.bt-palette-clear:hover{color:var(--bt-ink)}
--.bt-palette-clear svg{width:12px;height:12px}
--.bt-palette-list{position:relative;height:402px;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px;box-sizing:border-box}
--.bt-palette-list>.bt-thumb,.bt-drop-list>.bt-thumb{left:0;right:0;border-radius:8px;background:var(--bt-active)}
--.bt-palette-list>.bt-thumb{left:8px;right:8px}
--:is(.bt-palette-list,.bt-drop-list)[data-glide] [aria-selected=true]{background:transparent}
--:is(.bt-palette-row,.bt-palette-section,.bt-drop-list>.bt-option){position:relative;z-index:1}
--.bt-palette-section{padding:8px 8px 4px;font-size:12px;line-height:16px;color:var(--bt-ink-3);flex:none}
--.bt-palette-section:first-of-type{padding-top:0}
--.bt-palette-row{flex:none;display:flex;align-items:center;gap:8px;height:49px;padding:6px 10px 6px 8px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;box-sizing:border-box;width:100%}
--.bt-palette-row[aria-selected=true]{background:var(--bt-active)}
--.bt-palette-lead{flex:none;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}
--.bt-palette-cmd{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)}
--.bt-palette-cmd svg{width:14px;height:14px}
--.bt-palette-agent{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink)}
--.bt-palette-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
--.bt-palette-title{font-size:14px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-palette-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-palette-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink)}
--.bt-palette-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:0 16px;text-align:center;animation:bt-veil-in .22s ease-out}
--.bt-palette-empty-icon{color:var(--bt-line-2)}
--.bt-palette-empty-icon svg{width:32px;height:32px}
--.bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)}
--.bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)}
--@media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}}
--.bt-account-menu{width:224px}
--.bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex}
--.bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))}
--/* Popover keyframes move translate and scale, which compose with a transform the popover
-- already has. */
--@keyframes bt-pop-drop{from{translate:0 -4px;scale:.96}}
--@keyframes bt-pop-lift{from{translate:0 4px;scale:.96}}
--@keyframes bt-pop-out{to{opacity:0;scale:.97;filter:blur(2px)}}
--@keyframes bt-veil-in{from{opacity:0;filter:blur(6px)}}
--@keyframes bt-check-pop{from{opacity:0;scale:.4}}
--@media (prefers-reduced-motion:reduce){.bt-menu,.bt-avedit,.bt-react-strip{animation:bt-fade .1s ease-out}.bt-react-strip button,.bt-q-check,.bt-color[aria-pressed=true] span{animation:none}}
--.bt-brand:hover{background:var(--bt-hover)}
--.bt-drop-list{position:relative;display:flex;flex-direction:column;gap:2px}
--.bt-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px}
--.bt-pick{display:flex;flex-direction:column;align-items:center;gap:4px;padding:7px 2px 6px;border-radius:10px;border:1px solid transparent;background:none;color:var(--bt-ink);cursor:pointer;font:inherit;font-size:11px;line-height:14px;min-width:0}
--.bt-pick:hover{background:var(--bt-hover)}
--.bt-pick[aria-pressed=true]{border-color:var(--bt-line-2);background:var(--bt-hover)}
--.bt-pick>span:last-child:not(.bt-mark){max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-pick:hover .bt-mark[data-state=idle] .bt-eye{animation:bt-blink .5s linear 1}
--.bt-select{width:100%;height:34px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink);padding:0 8px;font:inherit;font-size:13px}
--.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:6px}
--.bt-swatch-sep{width:1px;height:22px;background:var(--bt-line-2);margin:0 2px}
--.bt-swatch-custom{position:relative;overflow:hidden;background:conic-gradient(#ff3e51,#ffaf38,#00c972,#1cc3b0,#2a92fe,#a97efe,#ff5eb1,#ff3e51);box-sizing:border-box}
--.bt-swatch-custom[data-on=true]{border-color:var(--bt-ink)}
--.bt-swatch-custom input{position:absolute;inset:-4px;width:calc(100% + 8px);height:calc(100% + 8px);opacity:0;cursor:pointer;border:0;padding:0}
--.bt-swatch-default{background:linear-gradient(135deg,var(--bt-card) 0 46%,var(--bt-line-2) 46% 54%,var(--bt-card) 54%);box-shadow:inset 0 0 0 1px var(--bt-line-2)}
--.bt-states{display:flex;flex-wrap:wrap;gap:4px}
--.bt-state{border:1px solid var(--bt-line);background:none;border-radius:999px;font:inherit;font-size:11px;line-height:16px;padding:2px 8px;color:var(--bt-ink-2);cursor:pointer}
--.bt-state:hover{color:var(--bt-ink)}
--.bt-state[aria-pressed=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)}
--.bt-theme-chip{position:relative;width:46px;height:30px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--bt-line-2);background:linear-gradient(90deg,var(--ls) 0 36%,var(--lb) 36%);overflow:hidden}
--.bt-theme-chip i{position:absolute;right:7px;bottom:7px;width:10px;height:10px;border-radius:50%;background:var(--la)}
--body[data-ds-dark-theme] .bt-theme-chip{background:linear-gradient(90deg,var(--ds) 0 36%,var(--db) 36%)}
--body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
--.bt-tabs{flex-wrap:wrap}
--.bt-soft:disabled{opacity:.4;cursor:default}
--.bt-group-role{margin-left:6px;font-size:11px;line-height:14px;padding:1px 5px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);vertical-align:1px}
--.bt-members{display:flex;flex-direction:column;gap:1px}
--.bt-member{display:flex;align-items:center;gap:4px;min-height:40px;padding:0 4px 0 6px;border-radius:8px;animation:bt-rise .18s ease-out}
--.bt-member:hover{background:var(--bt-hover)}
--.bt-member-name{flex:1;min-width:0;display:flex;align-items:center;gap:10px;border:0;background:none;padding:6px 0;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;cursor:pointer;text-align:left}
--.bt-member-name>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-chip-admin{background:var(--bt-tag-bg);color:var(--bt-tag-ink);border-color:transparent}
--.bt-mini{flex:none;border:0;background:none;border-radius:6px;padding:2px 6px;font:inherit;font-size:12px;line-height:18px;color:var(--bt-ink-2);cursor:pointer}
--.bt-mini:hover{background:var(--bt-active);color:var(--bt-ink)}
--.bt-member .bt-mini,.bt-member .bt-x{opacity:0;transition:opacity .15s ease}
--.bt-member:hover .bt-mini,.bt-member:hover .bt-x,.bt-member .bt-mini:focus-visible,.bt-member .bt-x:focus-visible{opacity:1}
--.bt-add{color:var(--bt-ink-2);margin-top:2px}
--.bt-add:hover,.bt-addlist .bt-option:hover{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-addlist{display:flex;flex-direction:column;margin-top:4px;padding:4px;border:1px solid var(--bt-line);border-radius:10px;background:var(--bt-card);animation:bt-rise .16s ease-out}
--.bt-seg{position:relative;display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--bt-hover)}
--.bt-motion-seg{width:216px}
--.bt-seg button{position:relative;z-index:1;flex:1;height:30px;border:0;border-radius:8px;background:none;font:inherit;font-size:13px;color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,box-shadow .15s ease,${spring(SPRING_TAP, "scale")}}
--.bt-seg button[aria-pressed=true]{background:var(--bt-card);color:var(--bt-ink);box-shadow:0 1px 3px rgba(0,0,0,.08)}
--.bt-seg button:disabled{opacity:.4;cursor:default}
--.bt-seg button:active:not(:disabled){scale:.96}
--.bt-seg>.bt-thumb{top:2px;bottom:2px;border-radius:8px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08)}
--.bt-seg[data-glide] button[aria-pressed=true]{background:transparent;box-shadow:none}
--@keyframes bt-rise{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
--@media (prefers-reduced-motion:reduce){.bt-member,.bt-addlist{animation:none}}
++
+// src/client/mention-style.js
+function chipKey(bot) {
+ if (/\s/.test(bot?.name)) return null;
@@ -3677,37 +3019,7 @@ diff --git a/client.js b/client.js
+ return `${anchor}{color:${inkText(look.color)}}${chipBase}${mark}`;
+ }).join("\n");
+}
-
--/* Memory dialog: a summary of every entry, the entries behind the menu, "Ask or update" below. */
--.bt-mem-row{display:flex;align-items:center;gap:8px;width:100%;height:36px;padding:0 8px 0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;box-sizing:border-box;transition:background-color .12s ease}
--.bt-mem-row:hover{background:var(--bt-hover)}
--.bt-mem-row>svg{flex:none;color:var(--bt-ink-2)}
--.bt-mem-row-copy{flex:1;text-align:left}
--.bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px}
--.bt-mem-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:61;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-mem-scrim{animation:bt-fade .2s ease-out}
--.bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
--.bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box}
--.bt-mem-head .bt-icon-btn{width:32px;height:32px;flex:none}
--.bt-mem-head [aria-haspopup] svg{width:18px;height:18px;stroke-width:2.6}
--.bt-mem-head>.bt-icon-btn:first-child{margin-left:-10px}
--.bt-mem-head h2{margin:0;font-size:17px;line-height:24px;font-weight:600;white-space:nowrap}
--.bt-mem-status{font-size:13px;line-height:18px;color:var(--bt-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-variant-numeric:tabular-nums}
--.bt-mem-head-gap{flex:1}
--.bt-mem-menu{position:absolute;top:52px;right:50px;min-width:220px;--bt-origin:top right}
--.bt-mem-menu button:disabled{opacity:.4;cursor:default;background:none}
--.bt-mem-body{flex:1;overflow-y:auto;padding:22px 28px 24px;scrollbar-width:thin;display:flex;flex-direction:column;gap:12px}
--.bt-mem-summary{transition:opacity .3s ease}
--.bt-mem-summary[data-stale]{opacity:.55}
--.bt-mem-summary h3{margin:20px 0 6px;font-size:17px;line-height:26px;font-weight:600}
--.bt-mem-summary h3:first-child{margin-top:0}
--.bt-mem-summary p{margin:0;font-size:15px;line-height:26px}
--.bt-mem-summary>*{animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both}
--.bt-mem-summary>:nth-child(2){animation-delay:.03s}
--.bt-mem-summary>:nth-child(3){animation-delay:.06s}
--.bt-mem-summary>:nth-child(4){animation-delay:.09s}
--.bt-mem-summary>:nth-child(5){animation-delay:.12s}
--.bt-mem-summary>:nth-child(6){animation-delay:.15s}
++
+// src/client/motion.js
+var import_react4 = require("react");
+var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
@@ -3894,52 +3206,31 @@ diff --git a/client.js b/client.js
+ animation.onfinish = () => {
+ delete node.dataset.morph;
+ };
-+ animation.oncancel = () => {
-+ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph;
-+ };
-+ });
-+}
-+function useLinger(value, ms) {
+ animation.oncancel = () => {
+ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph;
+ };
+ });
+ }
+ function useLinger(value, ms) {
+- const [kept, setKept] = (0, import_react.useState)(value);
+- (0, import_react.useEffect)(() => {
+ const [kept, setKept] = (0, import_react4.useState)(value);
+ (0, import_react4.useEffect)(() => {
-+ if (value) {
-+ setKept(value);
-+ return void 0;
-+ }
-+ const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms);
-+ return () => clearTimeout(timer);
-+ }, [value]);
-+ return [value || kept, !value && Boolean(kept)];
-+}
-+
-+// src/client/styles.js
-+var SHELL_CSS = `
-+body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)}
-+body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)}
-+${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")}
-+${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")}
-+.bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
-+.bt-mark svg{width:100%;height:100%;overflow:visible}
-+.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
+ if (value) {
+ setKept(value);
+ return void 0;
+@@ -1838,6 +1838,7 @@ ${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-
+ .bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
+ .bt-mark svg{width:100%;height:100%;overflow:visible}
+ .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
+.bt-mark-emoji{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;user-select:none}
-+.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
-+.bt-mark .bt-b{transform-origin:50% 100%}
-+.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
-+/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a
-+ mark only for its state; normal, the default, adds petting, pokes and eyes that follow
-+ the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */
-+body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
-+@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}}
-+@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}}
-+@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}}
-+@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
-+body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
-+body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
+ .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
+ .bt-mark .bt-b{transform-origin:50% 100%}
+ .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
+@@ -1856,6 +1856,14 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo
+ @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
+ body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
+ body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
+.bt-tree-item{position:relative;display:flex;align-items:center;min-width:0}
+.bt-tree-item>.bt-row{flex:1 1 auto;min-width:0}
+.bt-tree-toggle{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);font-size:16px;line-height:1;cursor:pointer}
@@ -3948,83 +3239,14 @@ diff --git a/client.js b/client.js
+.bt-tree-chev{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:16px;line-height:1;transition:transform .12s ease}
+.bt-tree-toggle[aria-expanded=false] .bt-tree-chev{transform:rotate(-90deg)}
+.bt-tree-leaf{cursor:default}
-+@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
-+/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
-+ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
-+ closed in a happy arc, and springs back when the pointer leaves. */
-+:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%}
-+.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out}
-+.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in}
-+.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s}
-+@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}}
-+.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out}
-+.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out}
-+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none}
-+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none}
-+.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite}
-+.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite}
-+.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite}
-+.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite}
-+.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite}
-+.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%}
-+.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite}
-+.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite}
-+.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite}
-+.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%}
-+.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite}
-+/* A question for you hops twice when it arrives and the badge keeps it in view after;
-+ a lively mark keeps hopping. */
-+.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
-+.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite}
-+.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
-+body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite}
-+.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1}
-+.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%}
-+@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}}
-+.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important}
-+.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important}
-+.bt-mark[data-poke]{cursor:pointer}
-+body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important}
-+body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
-+@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}}
-+@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}}
-+@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}}
-+@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}}
-+@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}}
-+@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}}
-+@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}}
-+@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}}
-+@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}}
-+@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}}
-+@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}}
-+@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}}
-+@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}}
-+/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */
-+@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}}
-+@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}}
-+@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}}
-+.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)}
-+.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))}
-+.bt-badge-admin svg{width:7px;height:7px}
-+.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none}
-+.bt-badge-main svg{width:100%;height:100%;overflow:visible}
-+.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke}
-+.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px}
-+.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0}
-+.bt-cluster>*{position:absolute}
-+.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums}
-+.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)}
-+.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0}
-+.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)}
-+.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important}
-+
-+.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
-+.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
+ @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
+ /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
+ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
+@@ -1933,15 +1933,23 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
+ .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+ .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
+ .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
+-.bt-brand-whale{display:inline-flex;color:var(--bt-accent)}
+.bt-brand-mark{display:inline-flex;line-height:0}
+.bt-flybot-mark{display:block;flex:none}
+.bt-flybot-mark .bt-flybot-wing{fill:#8FB2FF}
@@ -4033,325 +3255,131 @@ diff --git a/client.js b/client.js
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-wing{fill:#A9C2FF}
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-body{fill:#6E87FF}
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-eye{fill:#0F1530}
-+.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
-+.bt-round:hover{background:var(--bt-hover)}
-+.bt-round:active{transform:scale(.94)}
-+.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
-+.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
+ .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
+ .bt-round:hover{background:var(--bt-hover)}
+ .bt-round:active{transform:scale(.94)}
+ .bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
+ .bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
+-.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
+.bt-tile{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
-+.bt-tile:hover{background:var(--bt-hover)}
-+.bt-tile[aria-current=page]{background:var(--bt-active)}
+ .bt-tile:hover{background:var(--bt-hover)}
+ .bt-tile[aria-current=page]{background:var(--bt-active)}
+.bt-tile-unread{position:absolute;top:8px;right:12px}
-+.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
-+.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
-+.bt-row:hover{background:var(--bt-hover)}
-+.bt-row[aria-current=page]{background:var(--bt-active)}
-+.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
-+.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
-+.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
+ .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
+@@ -1950,27 +1950,32 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
+ .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
+ .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
+ .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-row-title.bt-row-title-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;overflow:hidden}
+.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)}
-+.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-+.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-+.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
-+.bt-unread-wait{background:var(--bt-star)}
+ .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
+ .bt-unread-wait{background:var(--bt-star)}
+.bt-shared-from,.bt-shared-wait{display:flex;flex-direction:column;gap:6px;padding:8px 0}
+.bt-shared-wait .bt-set-row{min-width:0}
+.bt-shared-wait .bt-set-copy{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
-+.bt-rail{align-items:center;padding:0 0 8px}
-+.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
-+.bt-side-foot{display:flex;padding-top:8px}
-+.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
-+.bt-connect:hover{background:var(--bt-hover)}
-+.bt-connect svg{flex:none;width:15px;height:15px}
+ .bt-rail{align-items:center;padding:0 0 8px}
+ .bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
+ .bt-side-foot{display:flex;padding-top:8px}
+ .bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
+ .bt-connect:hover{background:var(--bt-hover)}
+ .bt-connect svg{flex:none;width:15px;height:15px}
+-.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}
+-.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
+-.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
+.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}ms ${SPRING_SHAPE.easing}}
-+@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
+ @keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
+-@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}}
+@media (prefers-reduced-motion:reduce){}
-+
-+.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
-+.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
-+.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
-+.bt-pill:active{transform:scale(.965)}
-+.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
+
+ .bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
+ .bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
+ .bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
+ .bt-pill:active{transform:scale(.965)}
+ .bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
+.bt-pill:has(.bt-pill-name-face){height:auto;min-height:40px;align-items:flex-start}
+.bt-pill-name.bt-pill-name-face{min-width:0;white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
-+.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
-+.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
-+.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
-+.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)}
-+.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms}
-+.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)}
-+.bt-pill-mem-ico svg{width:16px;height:16px}
-+.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both}
-+@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}}
-+.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
-+@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}}
-+.bt-swap{color:var(--bt-ink-3);font-size:12px}
-+.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease}
-+.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
-+.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none}
-+.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)}
-+.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)}
-+@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}}
-+
-+.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
-+.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
+ .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
+ .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
+ .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
+@@ -1992,6 +1992,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
+
+ .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
+ .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
+.bt-event.bt-not-sent{gap:6px;color:var(--bt-ink-3)}
-+.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
-+.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
-+.bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
-+.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important}
-+.bt-bubble p{margin:0}
-+.bt-bubble p + p{margin-top:6px}
-+.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px}
-+.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap}
-+.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat}
-+.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0}
-+.bt-group-msg:first-child{margin-top:0}
-+.bt-lead{flex:none;display:flex;margin-bottom:1px}
-+button.bt-lead{border:0;background:none;padding:0;cursor:pointer}
-+/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */
-+.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px}
-+.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px}
-+.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+
-+.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
-+body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)}
-+@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}}
-+.bt-q-settled{animation:none}
-+.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0}
-+.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
-+.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere}
-+.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
-+.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-q-x svg{width:14px;height:14px}
-+.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
-+.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px}
-+.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease}
-+.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)}
-+.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px}
-+.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none}
-+button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0}
-+.bt-q-raised .bt-q-opt::before{display:none}
-+button.bt-q-opt:hover{background:var(--bt-hover)}
-+button.bt-q-opt:active{background:var(--bt-active)}
-+.bt-q-opt[aria-pressed=true]{background:var(--bt-active)}
-+.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)}
-+.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease}
-+.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px}
-+/* With a description the key spans the two text lines less 2px above and below. */
-+.bt-q-opt:has(.bt-q-desc){--bt-key:34px}
-+.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px}
-+.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0}
-+.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere}
-+.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere}
-+.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
-+.bt-q-settled .bt-q-check{animation:none}
-+.bt-q-check svg{width:16px;height:16px}
-+.bt-q-settled .bt-key{opacity:.4}
-+.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)}
-+.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)}
-+.bt-q.bt-q-settled{gap:6px;padding:12px 16px}
-+.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500}
-+.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0}
-+.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0}
-+.bt-q-settled .bt-q-opt{padding:4px 8px}
-+.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box}
-+.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0}
-+.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease}
-+.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)}
-+.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere}
-+.bt-q-input:not(:placeholder-shown){field-sizing:content}
-+.bt-q-input::placeholder{color:var(--bt-ink-3)}
-+.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards}
-+@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}}
-+.bt-q-submit:hover:not(:disabled){opacity:.86}
-+.bt-q-submit:disabled{opacity:.4;cursor:default}
-+.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-q-btn:hover{background:var(--bt-active)}
-+.bt-q-foot{display:flex;justify-content:flex-end;gap:8px}
-+.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)}
-+.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px}
-+.bt-q-raised .bt-q-head:last-child{padding-bottom:16px}
-+.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px}
-+.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)}
-+.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)}
-+.bt-q-raised .bt-q-body{padding:2px 6px 0}
-+.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)}
-+.bt-q-ownrow{cursor:text}
-+.bt-q-ownrow .bt-q-input{color:var(--bt-ink)}
-+.bt-q-raised .bt-q-foot{padding:0 8px 8px}
-+@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
-+
-+.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
+ .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
+ .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
+ .bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
+@@ -2085,6 +2085,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
+
+ .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
+.bt-pane[data-menu]{background:transparent}.bt-pane[data-menu] .bt-newchat{flex:none;align-self:flex-start;width:min(280px,calc(100% - 16px));max-height:calc(100% - 16px);margin:8px;overflow:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.06)}.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:0;box-shadow:none;border:0;max-width:none}
-+.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
-+.bt-to-label{color:var(--bt-ink-2)}
-+.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
-+.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px}
-+.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px}
-+.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
-+.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%}
-+.bt-option[aria-selected=true]{background:var(--bt-active)}
-+.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
-+.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
+ .bt-to-label{color:var(--bt-ink-2)}
+ .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
+@@ -2098,7 +2098,9 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-option[aria-selected=true]{background:var(--bt-active)}
+ .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
+ .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+-.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out}
+.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:0 1 auto;min-width:0;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;animation:bt-veil-in .16s ease-out}
+.bt-option-copy{flex:1;min-width:0;display:flex;flex-direction:column}
+.bt-option-sub{font-size:11px;line-height:15px;color:var(--bt-ink-2);white-space:normal;overflow-wrap:anywhere}
-+.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
-+.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
-+.bt-nc-spacer{flex:1}
-+.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center}
-+.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box}
-+.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none}
-+.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box}
-+.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
-+.bt-nc-send:disabled{opacity:.3;cursor:default}
-+.bt-create{flex:1;display:flex;flex-direction:column;min-width:0}
-+.bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box}
-+.bt-create-gap{width:28px}
-+.bt-create-title{font-size:14px;line-height:20px;font-weight:500}
-+.bt-create-body{flex:1;overflow-y:auto;padding:28px 16px 32px;display:flex;justify-content:center}
-+.bt-create-card{width:100%;max-width:400px;display:flex;flex-direction:column;gap:18px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .22s ease-out both}
-+.bt-create-preview{display:flex;flex-direction:column;align-items:center;gap:10px}
-+.bt-create-pop{display:inline-flex;animation:bt-create-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} both}
-+@keyframes bt-create-pop{from{transform:scale(.72);opacity:.4}}
-+.bt-create-name{font-size:17px;line-height:24px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-create-step{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-bottom:-10px}
-+.bt-create-looks{display:flex;flex-direction:column;gap:10px;padding:10px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)}
-+.bt-create-lookbar{justify-content:center}
-+.bt-create-foot{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}
-+.bt-create-go{height:36px;padding:0 20px}
-+@media (prefers-reduced-motion:reduce){.bt-create-card,.bt-create-pop{animation:none}}
-+.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
-+.bt-send:disabled{opacity:.35;cursor:default}
-+.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
+ .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
+ .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
+ .bt-nc-spacer{flex:1}
+@@ -2127,6 +2127,10 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
+ .bt-send:disabled{opacity:.35;cursor:default}
+ .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
+.bt-fix-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
+.bt-fix-btn{font:inherit;font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer}
+.bt-fix .bt-note{margin-top:6px}
+.bt-fix-link{margin-top:6px;color:inherit;user-select:all}
-+
-+.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0}
-+.bt-dialog .bt-head{height:62px}
-+.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box}
-+.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px}
-+.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both}
-+.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none}
-+@keyframes bt-sheet-in{from{transform:translateY(24px)}}
-+@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}}
-+.bt-exchange .bt-dialog{position:relative}
-+.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin}
-+.bt-exchange .bt-tools{display:none}
-+.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both}
-+.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none}
-+.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards}
-+@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}}
-+@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}}
-+@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}}
-+.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer}
-+.bt-soft:hover{background:var(--bt-active)}
-+.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0}
-+
+
+ .bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+ .bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0}
+@@ -2150,11 +2150,11 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-soft:hover{background:var(--bt-active)}
+ .bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0}
+
+-.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
+.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing};container:bt-panel/inline-size}
-+.bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none}
-+.bt-panel[data-instant]{transition:none}
-+@keyframes bt-pane-in{from{transform:translateX(100%)}}
+ .bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none}
+ .bt-panel[data-instant]{transition:none}
+ @keyframes bt-pane-in{from{transform:translateX(100%)}}
+-[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
+[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing};position:relative;z-index:0;isolation:isolate}
-+.bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none}
-+.bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)}
-+.bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize}
-+.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column}
-+.bt-drawer-top{display:flex;justify-content:flex-end;gap:8px;padding:12px 17px 6px 12px}
-+.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
-+.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
-+.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none}
+ .bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)}
+ .bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize}
+@@ -2163,6 +2163,8 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
+ .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
+ .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.bt-drawer-name-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;text-overflow:ellipsis;line-clamp:3;cursor:default}
+.bt-drawer-name-face:hover{background:none}
-+.bt-drawer-name:hover{background:var(--bt-hover)}
-+.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
-+.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
-+.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0}
-+.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap}
-+.bt-tab:hover{color:var(--bt-ink)}
-+.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)}
-+.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent}
-+.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px}
-+.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px}
-+.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px}
-+.bt-muted{color:var(--bt-ink-2)}
-+.bt-swatches{display:flex;flex-wrap:wrap;gap:6px}
-+.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)}
-+.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)}
-+.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical}
-+.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
-+.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)}
-+.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums}
-+.bt-danger{color:#e02135}
-+
-+/* Bot settings: fields, the avatar editor popover, and the details-pane sub-view. */
-+.bt-field{display:flex;flex-direction:column;gap:6px}
-+.bt-field-label{font-size:13px;line-height:18px;font-weight:500}
-+.bt-input{width:100%;box-sizing:border-box;height:36px;border:1px solid var(--bt-line);border-radius:8px;padding:0 10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;outline:none}
-+.bt-input:focus{border-color:var(--bt-line-2)}
-+.bt-input-area{height:auto;min-height:96px;padding:8px 10px;line-height:18px;resize:vertical}
-+.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-3)}
-+.bt-soft-sm{height:28px;padding:0 12px;font-size:12px}
-+.bt-set-avatar{display:flex;justify-content:center;padding:4px 0}
-+.bt-avatar-anchor{position:relative}
-+.bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0}
-+.bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)}
+ .bt-drawer-name:hover{background:var(--bt-hover)}
+ .bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
+ .bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
+@@ -2197,7 +2197,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-avatar-anchor{position:relative}
+ .bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0}
+ .bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)}
+-.bt-avedit{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:300px;background:var(--bt-main);border:1px solid var(--bt-line);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.18);padding:10px;z-index:70;display:flex;flex-direction:column;gap:10px;color:var(--bt-ink);transform-origin:top center;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
+.bt-avedit{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:300px;background:var(--bt-main);border:1px solid var(--bt-line);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.18);padding:10px;z-index:130;display:flex;flex-direction:column;gap:10px;color:var(--bt-ink);transform-origin:top center;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+.bt-bs .bt-avedit{transform-origin:32px 0}
-+.bt-avedit-top{display:flex;align-items:center;justify-content:space-between}
-+.bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px}
-+.bt-avedit-tab{position:relative;z-index:1;border:0;background:none;border-radius:999px;padding:3px 10px;font-size:12px;line-height:16px;color:var(--bt-ink-2);cursor:pointer}
-+.bt-avedit-tab[aria-selected=true]{background:var(--bt-main);color:var(--bt-ink)}
-+.bt-avedit-tabs>.bt-thumb{top:2px;bottom:2px;border-radius:999px;background:var(--bt-main);box-shadow:0 1px 2px rgba(0,0,0,.06)}
-+.bt-avedit-tabs[data-glide] .bt-avedit-tab[aria-selected=true]{background:transparent}
-+.bt-avedit-reset{color:var(--bt-ink-3)}
-+.bt-avedit-reset:hover{color:var(--bt-ink)}
-+.bt-shapes{display:flex;flex-direction:column;gap:2px}
-+.bt-shape-row{display:grid;grid-template-columns:repeat(6,1fr);gap:2px}
-+.bt-shape{position:relative;aspect-ratio:1;border:0;border-radius:8px;background:none;color:var(--bt-ink);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
-+.bt-shape:hover{background:var(--bt-hover)}
-+.bt-shape .bt-ring{position:absolute;left:50%;top:50%;width:36px;height:36px;translate:-50% -50%;overflow:visible;opacity:0;scale:.82;pointer-events:none;transition:opacity .14s ease,${spring(SPRING_POP, "scale")}}
-+.bt-shape[aria-pressed=true] .bt-ring{opacity:1;scale:1}
-+.bt-colors{display:flex;justify-content:space-between}
-+.bt-color{width:22px;height:22px;border-radius:50%;border:2px solid transparent;padding:2px;background:none;cursor:pointer;box-sizing:border-box;transition:border-color .14s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-color[aria-pressed=true] span{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-color span{display:block;width:100%;height:100%;border-radius:50%}
-+.bt-color[aria-pressed=true]{border-color:var(--bt-ink)}
-+.bt-dropzone{border:1.5px dashed var(--bt-line-2);border-radius:10px;padding:18px 12px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--bt-ink-2);font-size:12px;line-height:16px;text-align:center}
-+.bt-dropzone[data-over]{border-color:var(--bt-accent);color:var(--bt-ink)}
-+.bt-dropzone img{width:64px;height:64px;border-radius:50%;object-fit:cover}
-+.bt-set-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
-+.bt-about{white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
-+.bt-about[data-open]{display:block;-webkit-line-clamp:unset}
-+.bt-more{border:0;background:none;color:var(--bt-ink-3);font-size:12px;line-height:16px;cursor:pointer;padding:2px 0}
-+.bt-more:hover{color:var(--bt-ink)}
-+.bt-subhead{display:flex;align-items:center;gap:4px;padding:10px 12px 4px}
-+.bt-subhead-title{flex:1;font-size:15px;line-height:24px;font-weight:600;padding-left:4px}
-+.bt-icon-btn{width:28px;height:28px;border-radius:50%;border:0;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
-+.bt-icon-btn:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-set-body{gap:20px}
-+.bt-drawer-model{display:flex;flex-direction:column}
-+.bt-drawer-model .bt-note{margin-top:6px}
-+.bt-drawer-permission{display:flex;flex-direction:column}
-+.bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135}
-+
-+/* Settings dialog: a 198px page list beside the page. */
+ .bt-bs .bt-avedit{transform-origin:32px 0}
+ .bt-avedit-top{display:flex;align-items:center;justify-content:space-between}
+ .bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px}
+@@ -2237,9 +2237,23 @@ button.bt-q-opt:active{background:var(--bt-active)}
+ .bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135}
+
+ /* Settings dialog: a 198px page list beside the page. */
+-.bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:60;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+/* #1347: dialogs, sheets and scrims sit in a tier of their own, well clear of everything the
+ chat surface may do. They used to share 60/61 with the context menu, the reaction strip and the
+ remote-Bot sheet, so any body-level popover opened after them tied on z-index and won on DOM
@@ -4360,8 +3388,8 @@ diff --git a/client.js b/client.js
+ chat column is additionally capped at z-index 0 inside its own stacking context (see the
+ center-column rule below), so nothing in the thread can outrank these whatever it does. */
+.bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:120;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-scrim{position:absolute;inset:0;background:rgba(0,0,0,.32);animation:bt-fade .22s ease-out}
-+body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)}
+ .bt-scrim{position:absolute;inset:0;background:rgba(0,0,0,.32);animation:bt-fade .22s ease-out}
+ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)}
+/* #1347: a modal is a modal. A 0.32 scrim leaves the thread legible behind it, and the owner read that
+ as the chat painting through the dialog ("assistant text readable across columns", a pill visible at
+ full opacity behind the Inbox). The scrim now separates the two surfaces outright: the thread is
@@ -4369,68 +3397,34 @@ diff --git a/client.js b/client.js
+ still dismisses on a click, so nothing else about it changes. */
+body:not([data-ds-dark-theme]) .bt-scrim{background:rgba(12,12,14,.62)}
+body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.7)}
-+.bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
-+.bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))}
-+:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none}
-+:is(.bt-settings-layer,.bt-mem-layer)[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in}
-+:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]>[role=dialog]{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards}
-+@keyframes bt-dialog-rise{from{transform:translateY(12px) scale(.96)}}
-+@keyframes bt-dialog-sink{to{opacity:0;transform:translateY(6px) scale(.975);filter:blur(3px)}}
-+@media (prefers-reduced-motion:reduce){.bt-settings{transition:none}.bt-settings,.bt-scrim,.bt-settings-page>*{animation:bt-fade .12s ease-out!important}}
-+.bt-settings-nav{position:relative;width:198px;flex:none;padding:16px 8px;border-right:.5px solid var(--bt-line);display:flex;flex-direction:column;gap:2px;background:var(--bt-sidebar);box-sizing:border-box}
-+.bt-settings-item{position:relative;z-index:1;display:flex;align-items:center;gap:8px;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left;transition:background-color .12s ease,color .12s ease}
-+.bt-settings-item:hover{color:var(--bt-ink);background:var(--bt-hover)}
-+.bt-settings-item[aria-current=page]{background:var(--bt-active);color:var(--bt-ink)}
-+.bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)}
-+.bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent}
-+.bt-settings-shell{margin-top:auto}
+ .bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+ .bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))}
+ :is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none}
+@@ -2255,6 +2255,7 @@ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)}
+ .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)}
+ .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent}
+ .bt-settings-shell{margin-top:auto}
+.bt-settings-out{margin-left:auto;opacity:.6;font-size:12px}
-+/* Usage while another page is open: out of the flow at Usage's own width, so it keeps
-+ its layout, and not rendered. */
-+.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
-+/* The page column takes the dialog's final size at once, so while the dialog resizes
-+ only its edge moves and the page does not lay out again every frame. */
-+.bt-settings-main{flex:none;display:flex;flex-direction:column;min-width:0;width:calc(var(--bt-set-w) - 198px);height:var(--bt-set-h)}
-+.bt-settings-head{display:flex;align-items:center;gap:6px;padding:16px 56px 4px 20px}
-+.bt-settings-head h2{margin:0;font-size:15px;line-height:24px;font-weight:600;animation:bt-veil-in .2s ease-out}
-+.bt-settings-head>.bt-icon-btn{animation:bt-back-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+@keyframes bt-back-in{from{translate:6px 0;scale:.8}}
-+.bt-settings-scroll{flex:1;overflow-y:auto;padding:12px 20px 24px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin}
-+.bt-settings-page>:not(.bt-us){animation:bt-page-up ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .22s ease-out backwards}
-+.bt-settings-page[data-move=in]>:not(.bt-us){animation-name:bt-page-in,bt-veil-in}
-+.bt-settings-page[data-move=out]>:not(.bt-us){animation-name:bt-page-out,bt-veil-in}
-+.bt-settings-page>:nth-child(2){animation-delay:30ms}
-+.bt-settings-page>:nth-child(3){animation-delay:60ms}
-+.bt-settings-page>:nth-child(4){animation-delay:90ms}
-+.bt-settings-page>:nth-child(n+5){animation-delay:120ms}
-+@keyframes bt-page-up{from{transform:translateY(10px)}}
-+@keyframes bt-page-in{from{transform:translateX(18px)}}
-+@keyframes bt-page-out{from{transform:translateX(-18px)}}
-+.bt-settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
-+.bt-settings-close:hover{background:var(--bt-active);color:var(--bt-ink)}
-+.bt-settings-form{display:flex;flex-direction:column;gap:16px}
-+.bt-set-section h3{font-size:13px;line-height:18px;font-weight:600;margin:0 0 8px;padding:0 4px}
-+.bt-set-card{border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card);display:flex;flex-direction:column}
-+.bt-set-card>*+*{border-top:.5px solid var(--bt-line)}
-+.bt-set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;text-align:left;width:100%;box-sizing:border-box;border-radius:0}
-+button.bt-set-row{cursor:pointer}
-+button.bt-set-row:hover{background:var(--bt-hover)}
-+.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)}
-+.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px}
-+.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px}
+ /* Usage while another page is open: out of the flow at Usage's own width, so it keeps
+ its layout, and not rendered. */
+ .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
+@@ -2288,13 +2288,60 @@ button.bt-set-row:hover{background:var(--bt-hover)}
+ .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)}
+ .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px}
+ .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px}
+/* #892: a plugin's rows in the Members card (Invite by link) sit on the card's own padding, like a row. */
+.bt-flybot-room-members{padding:10px 12px}
-+.bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}
+ .bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}
+/* #1347: a row's copy never breaks its own label. The label used to be a bare inline span inside a
+ column that flexed down to 36px, so "Name" came out as "Nam" + "e"; a single-word label is now one
+ line, and the hint below it is what wraps when the column is short. */
+.bt-set-copy>.bt-bs-label,.bt-set-copy>.bt-set-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-+.bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))}
-+.bt-set-control{flex:none;display:flex;align-items:center;gap:6px}
-+.bt-set-value{font-size:13px;color:var(--bt-ink-2);text-align:right}
-+.bt-set-bot{flex:1;display:flex;align-items:center;gap:8px;min-width:0}
-+.bt-set-paste{flex-direction:column;align-items:stretch;gap:8px}
-+.bt-chevron{color:var(--bt-ink-3);display:inline-flex;flex:none}
+ .bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))}
+ .bt-set-control{flex:none;display:flex;align-items:center;gap:6px}
+ .bt-set-value{font-size:13px;color:var(--bt-ink-2);text-align:right}
+ .bt-set-bot{flex:1;display:flex;align-items:center;gap:8px;min-width:0}
+ .bt-set-paste{flex-direction:column;align-items:stretch;gap:8px}
+ .bt-chevron{color:var(--bt-ink-3);display:inline-flex;flex:none}
+/* Settings → Workflows (#1364): factory and your own pipelines, then the flowchart. */
+.bt-wf-page{display:flex;flex-direction:column;gap:16px}
+.bt-wf-hint{margin:0;font-size:12px;line-height:1.5;color:var(--bt-ink-2)}
@@ -4472,330 +3466,77 @@ diff --git a/client.js b/client.js
+.bt-wf-remove{position:absolute;top:-8px;right:-8px;width:18px;height:18px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-card);cursor:pointer;line-height:16px;padding:0}
+.bt-wf-par{position:absolute;border:1px dashed var(--bt-ink-3);border-radius:14px;pointer-events:none}
+.bt-wf-plabel{position:absolute;top:-9px;left:50%;transform:translateX(-50%);font-size:10.5px;color:var(--bt-ink-2);background:var(--bt-card);padding:0 6px;white-space:nowrap}
-+
-+/* Settings → Connectors. */
-+.bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px}
-+.bt-connector-head{display:flex;align-items:center;gap:12px;min-width:0}
-+.bt-connector-mark{flex:none;width:36px;height:36px;border-radius:10px;background:var(--bt-card);border:.5px solid var(--bt-line);display:flex;align-items:center;justify-content:center;color:var(--bt-ink)}
-+.bt-connector-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
-+.bt-connector-name{font-size:14px;line-height:20px;font-weight:500}
-+.bt-connector-status{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-connector[data-state=ready] .bt-connector-status{color:var(--bt-ink-2)}
-+.bt-connector[data-state=error] .bt-connector-status{color:#e02135;white-space:normal}
-+.bt-connector-actions{flex:none;display:flex;gap:6px}
-+.bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
-+.bt-connector-form-row{display:flex;align-items:center;gap:6px}
+
+ /* Settings → Connectors. */
+ .bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px}
+@@ -2308,8 +2308,17 @@ button.bt-set-row:hover{background:var(--bt-hover)}
+ .bt-connector-actions{flex:none;display:flex;gap:6px}
+ .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+ .bt-connector-form-row{display:flex;align-items:center;gap:6px}
+.bt-connector-apps{display:flex;flex-direction:column;gap:8px}
-+.bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)}
+ .bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)}
+.bt-connector-uses{padding-left:48px;font-size:12px;line-height:16px;color:var(--bt-ink-3)}
+.bt-connector-bots{display:flex;flex-direction:column;gap:2px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+.bt-connector-others{padding-left:20px}
-+.bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135}
+ .bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135}
+.bt-connector-app-list{list-style:none;margin:0;padding:0 0 0 48px;display:flex;flex-direction:column;gap:2px}
+.bt-connector-app{display:flex;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-2)}
+.bt-connector-app-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.bt-app-status{color:var(--bt-ink-3)}
+[data-status=sign-in]>.bt-app-status{color:#c46a00}
-+
-+/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the
-+ shell offers them and CSS-module name suffixes elsewhere. */
-+[class*="_logoRow"],[class*="_newSession"],nav[class*="_panelList"]{display:none!important}
-+[class*="_footArea"]{display:flex!important;flex-direction:row-reverse;align-items:center;gap:9px;padding:8px 6px 6px!important}
-+[class*="_footArea"]>[class*="_footerActions"]{flex:1 1 auto;min-width:0}
-+[class*="_footArea"]>[class*="_settingsArea"]{flex:none;width:auto!important}
-+[class*="_footArea"] button[aria-label="Settings"]{width:auto!important;padding:0!important;background:none!important;border-radius:50%!important;height:auto!important}
-+[class*="_widthHandle"]{display:none!important}
-+
-+.bt-voice-pane{flex-direction:column;align-items:center;justify-content:center;animation:bt-fade .2s ease-out}
-+.bt-voice-stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:24px;max-width:560px;text-align:center}
-+.bt-voice-orb{position:relative;width:200px;height:200px;display:grid;place-items:center;margin-bottom:8px}
-+.bt-voice-ring{position:absolute;inset:34px;border-radius:50%;background:var(--bt-hover);transform:scale(calc(1 + var(--bt-level,0) * .55));transition:transform .08s linear}
-+.bt-voice-ring-2{inset:14px;background:none;box-shadow:inset 0 0 0 1px var(--bt-line-2);transform:scale(calc(.94 + var(--bt-level,0) * .3));opacity:.8}
-+.bt-voice-stage[data-phase=thinking] .bt-voice-ring{animation:bt-voice-breathe 1.6s ease-in-out infinite}
-+.bt-voice-stage[data-phase=speaking] .bt-voice-ring{animation:bt-voice-speak .9s ease-in-out infinite}
-+@keyframes bt-voice-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
-+@keyframes bt-voice-speak{0%,100%{transform:scale(1.04)}30%{transform:scale(1.2)}60%{transform:scale(1.1)}}
-+.bt-voice-orb>.bt-mark{position:relative}
-+.bt-voice-name{font-size:16px;line-height:22px;font-weight:500}
-+.bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px}
-+.bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
-+.bt-voice-note{font-size:12px;color:#e02135}
-+.bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease}
-+.bt-voice-setup:hover{background:var(--bt-active)}
-+.bt-voice-bar{display:flex;gap:16px;padding:0 0 32px}
-+.bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease}
-+.bt-voice-btn:hover{background:var(--bt-active)}
-+.bt-voice-btn:active{transform:scale(.94)}
-+.bt-voice-btn[aria-pressed=true]{background:var(--bt-user);color:var(--bt-user-ink)}
-+.bt-voice-slash{position:absolute;width:24px;height:2px;border-radius:1px;background:currentColor;transform:rotate(-45deg)}
-+.bt-voice-end{background:#ff3e51;color:#fff}
-+.bt-voice-end:hover{background:#e02135}
-+@keyframes bt-fade{from{opacity:0}to{opacity:1}}
-+@keyframes bt-blur-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}}
-+@media (prefers-reduced-motion:reduce){.bt-voice-pane,.bt-voice-caption,.bt-voice-ring{animation:none!important}}
-+.bt-msg{display:flex;flex-direction:column;align-items:flex-start;max-width:min(80%,560px,calc(100% - 82px))}
-+/* The bubble's row: reactions hang below it, so the hover toolbar (and a group
-+ sender's avatar) line up with the bubble alone. */
-+.bt-msg-line{position:relative;display:flex;align-items:flex-end;gap:8px;max-width:100%}
-+.bt-msg-line>.bt-bubble{max-width:none;min-width:0}
-+/* The same bubble width as beside an avatar column: 80% of (100% - 30px), plus the 30px. */
-+.bt-astack[data-lead]>.bt-msg{max-width:min(calc(80% + 6px),590px,calc(100% - 52px))}
-+.bt-tools{position:absolute;top:50%;left:100%;margin-left:6px;transform:translateY(-50%);display:flex;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s}
-+.bt-tools::after{content:"";position:absolute;top:-8px;bottom:-8px;right:100%;width:128px;z-index:-1}
-+.bt-msg:hover>.bt-msg-line>.bt-tools,.bt-msg:focus-within>.bt-msg-line>.bt-tools,.bt-tools[data-open]{opacity:1;visibility:visible}
-+.bt-tool{width:24px;height:24px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease}
-+.bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-tool:active{background:var(--bt-active)}
-+.bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
+
+ /* Shell chrome reshaped for the team view. Selectors use stable data attributes where the
+ shell offers them and CSS-module name suffixes elsewhere. */
+@@ -2361,7 +2361,7 @@ button.bt-set-row:hover{background:var(--bt-hover)}
+ .bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)}
+ .bt-tool:active{background:var(--bt-active)}
+ .bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
+-.bt-react-strip{position:fixed;z-index:60;pointer-events:auto;display:flex;gap:2px;padding:4px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.08),0 4px 6px -4px rgba(0,0,0,.06);transform:translate(-50%,-100%);transform-origin:bottom center;animation:bt-pop-lift ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
+.bt-react-strip{position:fixed;z-index:130;pointer-events:auto;display:flex;gap:2px;padding:4px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.08),0 4px 6px -4px rgba(0,0,0,.06);transform:translate(-50%,-100%);transform-origin:bottom center;animation:bt-pop-lift ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
-+.bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
-+.bt-react-strip button:nth-child(2){animation-delay:20ms}
-+.bt-react-strip button:nth-child(3){animation-delay:40ms}
-+.bt-react-strip button:nth-child(4){animation-delay:60ms}
-+.bt-react-strip button:nth-child(n+5){animation-delay:80ms}
-+.bt-react-strip button:hover{background:var(--bt-hover);transform:scale(1.15)}
-+.bt-react-strip button:active{transform:scale(.92)}
-+.bt-reacts{display:flex;gap:4px;margin-top:4px}
-+.bt-react{height:22px;min-width:30px;padding:0 6px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);font-size:13px;line-height:20px;cursor:pointer;animation:bt-react-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing};transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-react:hover{background:var(--bt-hover)}
-+.bt-react:active{scale:.92}
-+@keyframes bt-react-pop{from{transform:scale(.6);opacity:0}}
-+.bt-bubble a[href^="${MENTION_ORIGIN}"] svg{display:none}
-+.bt-you{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,var(--bt-sidebar));color:var(--bt-ink-2);box-shadow:inset 0 0 0 1px var(--bt-line);transition:background-color .12s ease}
-+button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))}
-+.bt-you-wrap{display:inline-flex;padding:0}
-+.bt-connect-foot{width:100%;height:36px}
-+
+ .bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
+ .bt-react-strip button:nth-child(2){animation-delay:20ms}
+ .bt-react-strip button:nth-child(3){animation-delay:40ms}
+@@ -2380,7 +2380,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side
+ .bt-you-wrap{display:inline-flex;padding:0}
+ .bt-connect-foot{width:100%;height:36px}
+
+-.bt-menu{position:fixed;pointer-events:auto;z-index:60;min-width:200px;padding:6px;border-radius:12px;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),0 0 0 1px rgba(228,228,228,.04);display:flex;flex-direction:column;gap:2px;transform-origin:var(--bt-origin,top left);animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box}
+.bt-menu{position:fixed;pointer-events:auto;z-index:130;min-width:200px;padding:6px;border-radius:12px;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),0 0 0 1px rgba(228,228,228,.04);display:flex;flex-direction:column;gap:2px;transform-origin:var(--bt-origin,top left);animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box}
-+.bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box}
-+.bt-menu button:hover{background:var(--bt-hover)}
-+.bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px}
-+.bt-menu .bt-danger{color:#c21d2e}
-+.bt-menu-sep{height:.5px;flex:none;background:var(--bt-line-2);margin:4px 8px}
-+.bt-menu-up{--bt-origin:bottom left;animation-name:bt-pop-lift,bt-veil-in}
-+.bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards}
-+.bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums}
+ .bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box}
+ .bt-menu button:hover{background:var(--bt-hover)}
+ .bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px}
+@@ -2390,7 +2390,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side
+ .bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards}
+ .bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums}
+-.bt-palette-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:70;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+.bt-palette-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:140;isolation:isolate;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out}
-+.bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
-+.bt-palette-layer[data-leaving]{pointer-events:none}
-+.bt-palette-layer[data-leaving] .bt-backdrop{opacity:0;transition:opacity .16s ease-in}
-+.bt-palette-layer[data-leaving] .bt-palette{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards}
-+@keyframes bt-palette-in{from{translate:0 -10px;scale:.96}}
-+.bt-palette-head{display:flex;align-items:center;gap:4px;padding:14px 10px 14px 14px;border-bottom:.5px solid var(--bt-line-2)}
-+.bt-palette-glyph{display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;color:var(--bt-ink-3);margin-right:4px}
-+.bt-palette-head input{flex:1;min-width:0;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0}
-+.bt-palette-head input::placeholder{color:var(--bt-ink-3)}
-+.bt-palette-clear{width:18px;height:18px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
-+.bt-palette-clear:hover{color:var(--bt-ink)}
-+.bt-palette-clear svg{width:12px;height:12px}
-+.bt-palette-list{position:relative;height:402px;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px;box-sizing:border-box}
-+.bt-palette-list>.bt-thumb,.bt-drop-list>.bt-thumb{left:0;right:0;border-radius:8px;background:var(--bt-active)}
-+.bt-palette-list>.bt-thumb{left:8px;right:8px}
-+:is(.bt-palette-list,.bt-drop-list)[data-glide] [aria-selected=true]{background:transparent}
-+:is(.bt-palette-row,.bt-palette-section,.bt-drop-list>.bt-option){position:relative;z-index:1}
-+.bt-palette-section{padding:8px 8px 4px;font-size:12px;line-height:16px;color:var(--bt-ink-3);flex:none}
-+.bt-palette-section:first-of-type{padding-top:0}
-+.bt-palette-row{flex:none;display:flex;align-items:center;gap:8px;height:49px;padding:6px 10px 6px 8px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;box-sizing:border-box;width:100%}
-+.bt-palette-row[aria-selected=true]{background:var(--bt-active)}
-+.bt-palette-lead{flex:none;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}
-+.bt-palette-cmd{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)}
-+.bt-palette-cmd svg{width:14px;height:14px}
-+.bt-palette-agent{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink)}
-+.bt-palette-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
-+.bt-palette-title{font-size:14px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-palette-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-palette-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink)}
-+.bt-palette-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:0 16px;text-align:center;animation:bt-veil-in .22s ease-out}
-+.bt-palette-empty-icon{color:var(--bt-line-2)}
-+.bt-palette-empty-icon svg{width:32px;height:32px}
-+.bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)}
-+.bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)}
-+@media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}}
+ .bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out}
+ .bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+ .bt-palette-layer[data-leaving]{pointer-events:none}
+@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side
+ .bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)}
+ .bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)}
+ @media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}}
+-.bt-account-menu{width:224px}
+.bt-account-menu{min-width:224px;width:max-content;max-width:calc(100vw - 16px)}
+.bt-flybot-account-header:not(:empty){padding:6px 0 2px;margin-bottom:4px;border-bottom:.5px solid var(--bt-line-2)}
-+.bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex}
-+.bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))}
-+/* Popover keyframes move translate and scale, which compose with a transform the popover
-+ already has. */
-+@keyframes bt-pop-drop{from{translate:0 -4px;scale:.96}}
-+@keyframes bt-pop-lift{from{translate:0 4px;scale:.96}}
-+@keyframes bt-pop-out{to{opacity:0;scale:.97;filter:blur(2px)}}
-+@keyframes bt-veil-in{from{opacity:0;filter:blur(6px)}}
-+@keyframes bt-check-pop{from{opacity:0;scale:.4}}
-+@media (prefers-reduced-motion:reduce){.bt-menu,.bt-avedit,.bt-react-strip{animation:bt-fade .1s ease-out}.bt-react-strip button,.bt-q-check,.bt-color[aria-pressed=true] span{animation:none}}
-+.bt-brand:hover{background:var(--bt-hover)}
-+.bt-drop-list{position:relative;display:flex;flex-direction:column;gap:2px}
-+.bt-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px}
-+.bt-pick{display:flex;flex-direction:column;align-items:center;gap:4px;padding:7px 2px 6px;border-radius:10px;border:1px solid transparent;background:none;color:var(--bt-ink);cursor:pointer;font:inherit;font-size:11px;line-height:14px;min-width:0}
-+.bt-pick:hover{background:var(--bt-hover)}
-+.bt-pick[aria-pressed=true]{border-color:var(--bt-line-2);background:var(--bt-hover)}
-+.bt-pick>span:last-child:not(.bt-mark){max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-pick:hover .bt-mark[data-state=idle] .bt-eye{animation:bt-blink .5s linear 1}
-+.bt-select{width:100%;height:34px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink);padding:0 8px;font:inherit;font-size:13px}
-+.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:6px}
-+.bt-swatch-sep{width:1px;height:22px;background:var(--bt-line-2);margin:0 2px}
-+.bt-swatch-custom{position:relative;overflow:hidden;background:conic-gradient(#ff3e51,#ffaf38,#00c972,#1cc3b0,#2a92fe,#a97efe,#ff5eb1,#ff3e51);box-sizing:border-box}
-+.bt-swatch-custom[data-on=true]{border-color:var(--bt-ink)}
-+.bt-swatch-custom input{position:absolute;inset:-4px;width:calc(100% + 8px);height:calc(100% + 8px);opacity:0;cursor:pointer;border:0;padding:0}
-+.bt-swatch-default{background:linear-gradient(135deg,var(--bt-card) 0 46%,var(--bt-line-2) 46% 54%,var(--bt-card) 54%);box-shadow:inset 0 0 0 1px var(--bt-line-2)}
-+.bt-states{display:flex;flex-wrap:wrap;gap:4px}
-+.bt-state{border:1px solid var(--bt-line);background:none;border-radius:999px;font:inherit;font-size:11px;line-height:16px;padding:2px 8px;color:var(--bt-ink-2);cursor:pointer}
-+.bt-state:hover{color:var(--bt-ink)}
-+.bt-state[aria-pressed=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)}
-+.bt-theme-chip{position:relative;width:46px;height:30px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--bt-line-2);background:linear-gradient(90deg,var(--ls) 0 36%,var(--lb) 36%);overflow:hidden}
-+.bt-theme-chip i{position:absolute;right:7px;bottom:7px;width:10px;height:10px;border-radius:50%;background:var(--la)}
-+body[data-ds-dark-theme] .bt-theme-chip{background:linear-gradient(90deg,var(--ds) 0 36%,var(--db) 36%)}
-+body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
-+.bt-tabs{flex-wrap:wrap}
-+.bt-soft:disabled{opacity:.4;cursor:default}
-+.bt-group-role{margin-left:6px;font-size:11px;line-height:14px;padding:1px 5px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);vertical-align:1px}
-+.bt-members{display:flex;flex-direction:column;gap:1px}
-+.bt-member{display:flex;align-items:center;gap:4px;min-height:40px;padding:0 4px 0 6px;border-radius:8px;animation:bt-rise .18s ease-out}
-+.bt-member:hover{background:var(--bt-hover)}
-+.bt-member-name{flex:1;min-width:0;display:flex;align-items:center;gap:10px;border:0;background:none;padding:6px 0;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;cursor:pointer;text-align:left}
-+.bt-member-name>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-chip-admin{background:var(--bt-tag-bg);color:var(--bt-tag-ink);border-color:transparent}
-+.bt-mini{flex:none;border:0;background:none;border-radius:6px;padding:2px 6px;font:inherit;font-size:12px;line-height:18px;color:var(--bt-ink-2);cursor:pointer}
-+.bt-mini:hover{background:var(--bt-active);color:var(--bt-ink)}
-+.bt-member .bt-mini,.bt-member .bt-x{opacity:0;transition:opacity .15s ease}
-+.bt-member:hover .bt-mini,.bt-member:hover .bt-x,.bt-member .bt-mini:focus-visible,.bt-member .bt-x:focus-visible{opacity:1}
-+.bt-add{color:var(--bt-ink-2);margin-top:2px}
-+.bt-add:hover,.bt-addlist .bt-option:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-addlist{display:flex;flex-direction:column;margin-top:4px;padding:4px;border:1px solid var(--bt-line);border-radius:10px;background:var(--bt-card);animation:bt-rise .16s ease-out}
-+.bt-seg{position:relative;display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--bt-hover)}
-+.bt-motion-seg{width:216px}
-+.bt-seg button{position:relative;z-index:1;flex:1;height:30px;border:0;border-radius:8px;background:none;font:inherit;font-size:13px;color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,box-shadow .15s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-seg button[aria-pressed=true]{background:var(--bt-card);color:var(--bt-ink);box-shadow:0 1px 3px rgba(0,0,0,.08)}
-+.bt-seg button:disabled{opacity:.4;cursor:default}
-+.bt-seg button:active:not(:disabled){scale:.96}
-+.bt-seg>.bt-thumb{top:2px;bottom:2px;border-radius:8px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08)}
-+.bt-seg[data-glide] button[aria-pressed=true]{background:transparent;box-shadow:none}
-+@keyframes bt-rise{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
-+@media (prefers-reduced-motion:reduce){.bt-member,.bt-addlist{animation:none}}
-+
-+/* Memory dialog: a summary of every entry, the entries behind the menu, "Ask or update" below. */
-+.bt-mem-row{display:flex;align-items:center;gap:8px;width:100%;height:36px;padding:0 8px 0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;box-sizing:border-box;transition:background-color .12s ease}
-+.bt-mem-row:hover{background:var(--bt-hover)}
-+.bt-mem-row>svg{flex:none;color:var(--bt-ink-2)}
-+.bt-mem-row-copy{flex:1;text-align:left}
-+.bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px}
+ .bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex}
+ .bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))}
+ /* Popover keyframes move translate and scale, which compose with a transform the popover
+@@ -2494,7 +2494,7 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
+ .bt-mem-row>svg{flex:none;color:var(--bt-ink-2)}
+ .bt-mem-row-copy{flex:1;text-align:left}
+ .bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px}
+-.bt-mem-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:61;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+.bt-mem-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-mem-scrim{animation:bt-fade .2s ease-out}
-+.bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
-+.bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box}
-+.bt-mem-head .bt-icon-btn{width:32px;height:32px;flex:none}
-+.bt-mem-head [aria-haspopup] svg{width:18px;height:18px;stroke-width:2.6}
-+.bt-mem-head>.bt-icon-btn:first-child{margin-left:-10px}
-+.bt-mem-head h2{margin:0;font-size:17px;line-height:24px;font-weight:600;white-space:nowrap}
-+.bt-mem-status{font-size:13px;line-height:18px;color:var(--bt-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-variant-numeric:tabular-nums}
-+.bt-mem-head-gap{flex:1}
-+.bt-mem-menu{position:absolute;top:52px;right:50px;min-width:220px;--bt-origin:top right}
-+.bt-mem-menu button:disabled{opacity:.4;cursor:default;background:none}
-+.bt-mem-body{flex:1;overflow-y:auto;padding:22px 28px 24px;scrollbar-width:thin;display:flex;flex-direction:column;gap:12px}
-+.bt-mem-summary{transition:opacity .3s ease}
-+.bt-mem-summary[data-stale]{opacity:.55}
-+.bt-mem-summary h3{margin:20px 0 6px;font-size:17px;line-height:26px;font-weight:600}
-+.bt-mem-summary h3:first-child{margin-top:0}
-+.bt-mem-summary p{margin:0;font-size:15px;line-height:26px}
-+.bt-mem-summary>*{animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both}
-+.bt-mem-summary>:nth-child(2){animation-delay:.03s}
-+.bt-mem-summary>:nth-child(3){animation-delay:.06s}
-+.bt-mem-summary>:nth-child(4){animation-delay:.09s}
-+.bt-mem-summary>:nth-child(5){animation-delay:.12s}
-+.bt-mem-summary>:nth-child(6){animation-delay:.15s}
- .bt-mem-summary>:nth-child(n+7){animation-delay:.18s}
- @keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}}
- .bt-mem-skel{display:flex;flex-direction:column;gap:24px}
-@@ -2595,2185 +2595,4316 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
- :is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96}
- @media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}}
- `;
--var CONVERSATION_CSS = `
--.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
--.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
--.bt-think summary::-webkit-details-marker{display:none}
--.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
--.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
--.bt-event>span{padding:0 2px}
--.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}
--.bt-event button:hover{background:var(--bt-hover)}
--.bt-event button:active{background:var(--bt-active)}
--.bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px}
--.bt-event-whole>*{flex:none}
--.bt-event-whole>span:first-child{margin-right:2px}
--/* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */
--.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}
--[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap}
--.bt-activity{display:flex;align-items:center;gap:10px;margin:8px 0 4px 2px;font-size:14px;color:var(--bt-ink-2)}
--.bt-group-replies{display:contents}
--/* A Bot chat is flat in every work-details mode, without changing the user's mode
-- (it is a DSH-wide setting): step groups lose their header and stay open, and what a
-- mode folds away in a finished turn stays shown. TurnProcessCell also opens that
-- fold; these rules cover the first frame before it does. */
--[data-step-process]>div:first-child{display:none!important}
--[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
--:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
--[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
--[class*="_header"]:has(.bt-head){border-bottom-color:transparent!important;box-shadow:none!important}
--/* The transcript runs under a floating header pill. */
--header[class*="_header"]:has(.bt-head){position:absolute!important;top:0;left:0;right:0;z-index:8;display:flex!important;padding:10px 0 0!important;background:transparent!important;pointer-events:none;justify-content:center}
--header[class*="_header"]:has(.bt-head)::before{content:"";position:absolute;inset:0 0 auto;height:60px;background:linear-gradient(to bottom,var(--bt-main),color-mix(in srgb,var(--bt-main) 0%,transparent));pointer-events:none;z-index:-1}
--header[class*="_header"]:has(.bt-head)>[class*="_headerLeading"]{position:absolute;left:12px;top:10px;pointer-events:auto}
--header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
--[class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)}
--/* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators),
-- without its copy and branch row; a tail with nothing in it takes no room. */
--[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]){display:none!important}
--[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))){display:none!important}
--/* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not
-- the shell's new-session hero: no headline, no workspace or mode picker, and the
-- composer stays docked at the bottom. */
--[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
--[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
--[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
--[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
--/* The right sides give back the scrollbar gutter so both edges sit 16px from the column. */
--[class*="_scroll"]:has(> [class*="_column"]){padding:60px 11px 16px 16px!important}
--/* The shell's running status only hosts the activity row (.bt-typing). */
--[data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important}
--[data-chat-running]>span:not([role=status]){display:none!important}
--[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important}
--.bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards}
--.bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards}
--.bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0}
--.bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out}
--.bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}
--.bt-typing>.bt-mark .bt-eyes{transition:scale .28s cubic-bezier(.4,.06,.18,1)}
--.bt-typing>.bt-mark:hover .bt-eyes{scale:1.18}
--.bt-typing-text{display:flex;align-items:center;flex:1 1 auto;min-width:0;animation:bt-typing-label .22s ease-out .2s backwards}
--.bt-typing-label{display:inline-flex;align-items:center;gap:6px;min-width:0;animation:bt-typing-label .22s ease-out backwards}
--.bt-typing-label>.bt-shimmer{font-size:14px;line-height:22px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-typing-time{flex-shrink:0;font-size:12px;line-height:22px;color:var(--bt-ink-3);white-space:nowrap}
--@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)}}
--/* Shimmer: a stepped sweep clipped to the text. */
--.bt-shimmer{color:transparent;background:linear-gradient(90deg,var(--bt-shimmer-base) 0%,var(--bt-shimmer-base) 25%,var(--bt-ink) 60%,var(--bt-shimmer-base) 75%,var(--bt-shimmer-base) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;animation:bt-shimmer 2.2s steps(60) infinite;--bt-shimmer-base:color-mix(in srgb,var(--bt-ink) 40%,transparent)}
--@keyframes bt-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
--/* New rows slide up; a reply that replaces the activity row grows out of its corner. */
--[data-bt-enter="new"]{animation:bt-row-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--[data-bt-enter="after-collapse"]{transform-origin:0 100%;animation:bt-row-after .24s cubic-bezier(.23,1,.32,1) .38s backwards}
--@keyframes bt-row-in{from{transform:translateY(12px)}to{transform:none}}
--@keyframes bt-row-after{0%{opacity:0;transform:translateY(12px) scale(.94)}55%{opacity:1}100%{opacity:1;transform:none}}
--@media (prefers-reduced-motion:reduce){
--.bt-typing,.bt-typing[data-exiting]{animation-name:bt-fade-in;animation-duration:.12s;transform:none}
--.bt-typing[data-exiting]{animation-name:bt-fade-out}
--.bt-typing-text,.bt-typing-label{animation-name:bt-fade-in;animation-delay:0s}
--.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)}
--[data-bt-enter="new"]{animation:none}
--[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out}
-+var CONVERSATION_CSS = `
-+.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
-+.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
-+.bt-think summary::-webkit-details-marker{display:none}
+ .bt-mem-scrim{animation:bt-fade .2s ease-out}
+ .bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+ .bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box}
+@@ -2599,6 +2599,14 @@ var CONVERSATION_CSS = `
+ .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
+ .bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
+ .bt-think summary::-webkit-details-marker{display:none}
+.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1}
+.bt-think[open] .bt-think-caret{transform:rotate(90deg)}
+.bt-person-mark{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-ink-5,rgba(20,20,20,.08));color:var(--bt-ink-3,rgba(20,20,20,.45))}
@@ -4804,28 +3545,27 @@ diff --git a/client.js b/client.js
+/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
+.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
+.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))}
-+.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
-+.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
-+.bt-event>span{padding:0 2px}
-+.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}
-+.bt-event button:hover{background:var(--bt-hover)}
-+.bt-event button:active{background:var(--bt-active)}
-+.bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px}
-+.bt-event-whole>*{flex:none}
-+.bt-event-whole>span:first-child{margin-right:2px}
-+/* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */
-+.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}
-+[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap}
-+.bt-group-replies{display:contents}
-+/* A Bot chat is flat in every work-details mode, without changing the user's mode
-+ (it is a DSH-wide setting): step groups lose their header and stay open, and what a
-+ mode folds away in a finished turn stays shown. TurnProcessCell also opens that
-+ fold; these rules cover the first frame before it does. */
-+[data-step-process]>div:first-child{display:none!important}
-+[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
-+:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
-+[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
+ .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
+ .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
+ .bt-event>span{padding:0 2px}
+@@ -2612,7 +2612,6 @@ var CONVERSATION_CSS = `
+ .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}
+ [class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap}
+-.bt-activity{display:flex;align-items:center;gap:10px;margin:8px 0 4px 2px;font-size:14px;color:var(--bt-ink-2)}
+ .bt-group-replies{display:contents}
+ /* A Bot chat is flat in every work-details mode, without changing the user's mode
+ (it is a DSH-wide setting): step groups lose their header and stay open, and what a
+@@ -2622,21 +2622,48 @@ var CONVERSATION_CSS = `
+ [data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
+ :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
+ [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
+-[class*="_header"]:has(.bt-head){border-bottom-color:transparent!important;box-shadow:none!important}
+-/* The transcript runs under a floating header pill. */
+-header[class*="_header"]:has(.bt-head){position:absolute!important;top:0;left:0;right:0;z-index:8;display:flex!important;padding:10px 0 0!important;background:transparent!important;pointer-events:none;justify-content:center}
+-header[class*="_header"]:has(.bt-head)::before{content:"";position:absolute;inset:0 0 auto;height:60px;background:linear-gradient(to bottom,var(--bt-main),color-mix(in srgb,var(--bt-main) 0%,transparent));pointer-events:none;z-index:-1}
+-header[class*="_header"]:has(.bt-head)>[class*="_headerLeading"]{position:absolute;left:12px;top:10px;pointer-events:auto}
+-header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+/* FlyBot's pill rides the shell's own Session header utilities, beside the seats
+ the native header renders (actions: the subagent catalog; lineage; corner).
+ It is a compact control in that row now, not a floating replacement for the
@@ -4845,9 +3585,11 @@ diff --git a/client.js b/client.js
+ line. No fill, no radius, muted ink, and the description stays on hover. */
+[data-slot="conversation.session.header.actions"] [class*="_label"]{background:none;border-radius:0;color:var(--bt-ink-3);cursor:help;pointer-events:auto}
+[data-slot="conversation.session.header.actions"] [class*="_label"] svg{opacity:.7}
-+[class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)}
-+/* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators),
-+ without its copy and branch row; a tail with nothing in it takes no room. */
+ [class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)}
+ /* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators),
+ without its copy and branch row; a tail with nothing in it takes no room. */
+-[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]){display:none!important}
+-[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))){display:none!important}
+[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]):not(.bt-typing){display:none!important}
+[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))):not(:has([data-turn-tail]>.bt-typing)){display:none!important}
+/* #1347: the thread must never paint above a dialog, a sheet or a scrim.
@@ -4863,241 +3605,106 @@ diff --git a/client.js b/client.js
+ whatever any single bubble does. contain:paint is deliberately NOT used: it would clip the
+ popovers the thread legitimately renders (reaction strip, message menu), which portal to body
+ for exactly that reason. */
-+/* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not
-+ the shell's new-session hero: no headline, no workspace or mode picker, and the
-+ composer stays docked at the bottom. */
-+[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
+ /* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not
+ the shell's new-session hero: no headline, no workspace or mode picker, and the
+ composer stays docked at the bottom. */
+ [data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
+body[data-bt-remote-chat=empty][data-content-phase=hero] [class*="_scrollBody"]::before{content:var(--bt-remote-empty);display:block;text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:16px 12px 8px;max-width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto;white-space:pre-wrap}
-+[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
-+[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
-+[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
-+/* The right sides give back the scrollbar gutter so both edges sit 16px from the column. */
-+[class*="_scroll"]:has(> [class*="_column"]){padding:60px 11px 16px 16px!important}
-+/* The shell's running status only hosts the activity row (.bt-typing). */
-+[data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important}
-+[data-chat-running]>span:not([role=status]){display:none!important}
-+[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important}
+ [data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
+ [data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
+ [data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
+@@ -2646,8 +2646,14 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+ [data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important}
+ [data-chat-running]>span:not([role=status]){display:none!important}
+ [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important}
+.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}
+.bt-busy-ack .bt-bubble{opacity:.92}
-+.bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards}
-+.bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards}
+ .bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards}
+ .bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards}
+.bt-typing[data-carry],.bt-typing[data-carry]>.bt-typing-text{animation:none}
+/* Between a friend's chat turns the row sits in the last turn's tail (#443), at the transcript's foot. */
+[data-turn-tail]>.bt-typing{margin-top:var(--dsh-chat-flow-gap,12px)}
-+.bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0}
-+.bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out}
-+.bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}
-+.bt-typing>.bt-mark .bt-eyes{transition:scale .28s cubic-bezier(.4,.06,.18,1)}
-+.bt-typing>.bt-mark:hover .bt-eyes{scale:1.18}
-+.bt-typing-text{display:flex;align-items:center;flex:1 1 auto;min-width:0;animation:bt-typing-label .22s ease-out .2s backwards}
-+.bt-typing-label{display:inline-flex;align-items:center;gap:6px;min-width:0;animation:bt-typing-label .22s ease-out backwards}
-+.bt-typing-label>.bt-shimmer{font-size:14px;line-height:22px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-typing-time{flex-shrink:0;font-size:12px;line-height:22px;color:var(--bt-ink-3);white-space:nowrap}
-+@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)}}
-+/* Shimmer: a stepped sweep clipped to the text. */
-+.bt-shimmer{color:transparent;background:linear-gradient(90deg,var(--bt-shimmer-base) 0%,var(--bt-shimmer-base) 25%,var(--bt-ink) 60%,var(--bt-shimmer-base) 75%,var(--bt-shimmer-base) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;animation:bt-shimmer 2.2s steps(60) infinite;--bt-shimmer-base:color-mix(in srgb,var(--bt-ink) 40%,transparent)}
-+@keyframes bt-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
-+/* New rows slide up; a reply that replaces the activity row grows out of its corner. */
-+[data-bt-enter="new"]{animation:bt-row-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
-+[data-bt-enter="after-collapse"]{transform-origin:0 100%;animation:bt-row-after .24s cubic-bezier(.23,1,.32,1) .38s backwards}
-+@keyframes bt-row-in{from{transform:translateY(12px)}to{transform:none}}
-+@keyframes bt-row-after{0%{opacity:0;transform:translateY(12px) scale(.94)}55%{opacity:1}100%{opacity:1;transform:none}}
-+@media (prefers-reduced-motion:reduce){
-+.bt-typing,.bt-typing[data-exiting]{animation-name:bt-fade-in;animation-duration:.12s;transform:none}
-+.bt-typing[data-exiting]{animation-name:bt-fade-out}
-+.bt-typing-text,.bt-typing-label{animation-name:bt-fade-in;animation-delay:0s}
-+.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)}
-+[data-bt-enter="new"]{animation:none}
-+[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out}
-+}
-+@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}
+ .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0}
+ .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out}
+ .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}
+@@ -2679,7 +2679,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+ @keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
+ @keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
+ [data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
+-[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
+[data-composer-card]:hover{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
+[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 55%,transparent)!important}
-+[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
-+[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
-+[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
-+[data-composer-card]>[class*="_row"]{display:contents!important}
-+[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0}
-+[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important}
-+[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0}
-+[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%}
-+[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
-+[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
-+[data-composer-placeholder]{font-size:0!important;line-height:0!important}
+ [data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
+ [class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
+ [class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
+@@ -2691,6 +2691,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+ [data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
+ [data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
+ [data-composer-placeholder]{font-size:0!important;line-height:0!important}
+/* A friend's Bot chat: no local run stats (they are relay turns, always 0; #419). */
+body[data-bt-friend-chat] [data-composer-stats]{display:none!important}
-+[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
-+/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
-+[data-composer-card] button[class*="_add"]{display:none!important}
-+.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
-+.bt-plus:active{scale:.9}
-+.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}}
-+.bt-plus[aria-expanded=true] svg{rotate:45deg}
-+.bt-plus-menu{min-width:180px}
-+[data-composer-card] [class*="_tools"]{gap:0!important}
-+[data-composer-card] [class*="_modes"]:empty{display:none!important}
-+[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important}
-+[data-composer-card] [class*="_trailing"]{gap:8px}
-+[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px}
-+[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9}
-+[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9}
-+@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}}
-+[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px}
-+[data-composer-card] button[class*="_primary"]:disabled{display:none!important}
-+[data-composer-dock]{display:none!important}
-+.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)}
-+.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
-+.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite}
-+@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}}
-+.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-voice:hover{opacity:.9}
-+.bt-mic:active,.bt-voice:active{scale:.9}
-+[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
-+body[data-bt-room] .bt-voice{display:none!important}
+ [data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
+ /* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
+ [data-composer-card] button[class*="_add"]{display:none!important}
+@@ -2722,7 +2722,9 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+ .bt-mic:active,.bt-voice:active{scale:.9}
+ [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
+ body[data-bt-room] .bt-voice{display:none!important}
+body[data-bt-room] [data-composer-card] [class*="_activity"]{display:none!important}
-+body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)}
+ body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)}
+body[data-bt-workflow] [data-composer-card]{display:none!important}
-+.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out}
-+[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2}
-+.bt-q{order:1}
-+div:has(> nav [class*="_marks"]){display:none!important}
-+/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies)
-+ takes its surface and its text from the one --bt-user pair: a skin that repaints
-+ the host's bubble surface alone would otherwise leave the text unreadable. */
-+[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important}
-+[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit}
-+/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */
-+[class*="_userRow"] [class*="_actions"]{display:none!important}
-+[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start}
-+[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none}
-+[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible}
-+[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none}
-+[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)}
-+[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px}
-+.bt-q-inline{display:flex;margin:4px 0 6px}
-+[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important}
-+[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important}
-+.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0}
-+.bt-q-dock{width:100%;display:flex;justify-content:center}
-+.bt-q-dock .bt-q{margin:0 auto 8px}
-+[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important}
-+[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important}
-+[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}}
-+[class*="_toBottomSlot"]>button:active{scale:.9}
-+[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important}
-+[class*="_toBottomSlot"]>button svg{display:none}
-+[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat}
-+@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}}
-+[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important}
-+.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+@keyframes bt-news-in{from{translate:0 8px;scale:.9}}
-+.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in}
-+@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}}
-+.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none}
-+.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px}
-+.bt-news-ico svg{width:16px;height:16px}
-+.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
-+.bt-news-x svg{width:12px;height:12px}
-+.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)}
-+@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}}
-+.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)}
-+.bt-open-new{display:flex;flex-direction:column}
-+.bt-open-new .bt-new-sep{margin:18px 0 0}
-+.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px}
-+.bt-earlier{display:flex;flex-direction:column;gap:6px}
-+.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
-+.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
-+.bt-urow{display:flex;justify-content:flex-end}
+ .bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out}
+ [data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2}
+ .bt-q{order:1}
+@@ -2776,6 +2776,7 @@ div:has(> nav [class*="_marks"]){display:none!important}
+ .bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
+ .bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
+ .bt-urow{display:flex;justify-content:flex-end}
+.bt-urow>.bt-lead{align-self:flex-end;margin-left:6px}
-+.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
-+.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
-+.bt-ububble p{margin:0;line-height:22px}
-+.bt-uimages{display:block;font-size:12px;opacity:.7}
-+.bt-earlier .bt-event{margin:4px 0}
-+.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)}
-+.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)}
-+`;
-+function installStyles(text, tag) {
-+ const style = document.createElement("style");
-+ style.dataset.dshBot = tag;
-+ style.textContent = text;
-+ document.head.appendChild(style);
-+ return style;
-+}
-+var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g;
-+var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g;
-+function sheetClassNames(sheet) {
-+ const names = [];
-+ const visit = (rules) => {
-+ for (const rule of rules) {
-+ if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]);
-+ if (rule.cssRules) visit(rule.cssRules);
-+ }
-+ };
-+ try {
-+ visit(sheet.cssRules);
-+ } catch {
-+ }
-+ return names;
-+}
-+function compileFragmentSelectors(text, names) {
-+ const compiled = /* @__PURE__ */ new Map();
-+ return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => {
-+ if (!compiled.has(fragment)) {
-+ const exact = [];
-+ for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`);
-+ compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole);
-+ }
-+ return compiled.get(fragment);
-+ });
-+}
-+function installCompiledStyles(text, tag) {
-+ const style = installStyles("", tag);
-+ const cache = /* @__PURE__ */ new WeakMap();
-+ const render = () => {
-+ const names = /* @__PURE__ */ new Set();
-+ for (const sheet of document.styleSheets) {
-+ if (sheet.ownerNode?.dataset?.dshBot) continue;
-+ let list = cache.get(sheet);
-+ if (!list) {
-+ list = sheetClassNames(sheet);
-+ cache.set(sheet, list);
-+ }
-+ for (const name of list) names.add(name);
-+ }
-+ const next = compileFragmentSelectors(text, names);
-+ if (style.textContent !== next) style.textContent = next;
-+ };
-+ render();
-+ let timer;
-+ const schedule = () => {
-+ clearTimeout(timer);
-+ timer = setTimeout(render, 120);
-+ };
-+ const onLoad = (event) => {
-+ if (event.target instanceof HTMLLinkElement) schedule();
-+ };
-+ const observer = new MutationObserver(schedule);
-+ observer.observe(document.head, { childList: true });
-+ document.addEventListener("load", onLoad, true);
-+ return () => {
-+ observer.disconnect();
-+ document.removeEventListener("load", onLoad, true);
-+ clearTimeout(timer);
-+ style.remove();
-+ };
-+}
-+
+ .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
+ .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
+ .bt-ububble p{margin:0;line-height:22px}
+@@ -2855,1925 +2855,4018 @@ function installCompiledStyles(text, tag) {
+ };
+ }
+
+-// 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/shared/face-title.js
+var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+function isGenericFaceTitle(name) {
@@ -5128,7 +3735,32 @@ diff --git a/client.js b/client.js
+ }
+ rows.sort((a, b) => a.sort.localeCompare(b.sort));
+ return rows.slice(0, 3).map((row) => row.owner);
-+}
+ }
+-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 uniqueOwners(owners) {
+ const seen = /* @__PURE__ */ new Set();
+ const out = [];
@@ -5206,8 +3838,13 @@ diff --git a/client.js b/client.js
+ return () => {
+ offSessions?.();
+ offRoster?.();
-+ };
-+}
+ };
+- 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)(
+function installBrandChrome() {
+ if (typeof document === "undefined") return () => {
+ };
@@ -5316,7 +3953,9 @@ diff --git a/client.js b/client.js
+var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
+function PinMarkIcon() {
+ return (0, import_react5.createElement)(
-+ "svg",
+ "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" })
+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
+ (0, import_react5.createElement)(
+ "g",
@@ -5324,122 +3963,13 @@ diff --git a/client.js b/client.js
+ (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
+ (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
+ )
-+ );
+ );
}
--@keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
--@keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
--[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
--[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
--[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
--[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
--[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
--[data-composer-card]>[class*="_row"]{display:contents!important}
--[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0}
--[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important}
--[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0}
--[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%}
--[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
--[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
--[data-composer-placeholder]{font-size:0!important;line-height:0!important}
--[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
--/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
--[data-composer-card] button[class*="_add"]{display:none!important}
--.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
--.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
--.bt-plus:active{scale:.9}
--.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}}
--.bt-plus[aria-expanded=true] svg{rotate:45deg}
--.bt-plus-menu{min-width:180px}
--[data-composer-card] [class*="_tools"]{gap:0!important}
--[data-composer-card] [class*="_modes"]:empty{display:none!important}
--[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important}
--[data-composer-card] [class*="_trailing"]{gap:8px}
--[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px}
--[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9}
--[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9}
--@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}}
--[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px}
--[data-composer-card] button[class*="_primary"]:disabled{display:none!important}
--[data-composer-dock]{display:none!important}
--.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
--.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)}
--.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
--.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite}
--@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}}
--.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--.bt-voice:hover{opacity:.9}
--.bt-mic:active,.bt-voice:active{scale:.9}
--[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
--body[data-bt-room] .bt-voice{display:none!important}
--body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)}
--.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out}
--[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2}
--.bt-q{order:1}
--div:has(> nav [class*="_marks"]){display:none!important}
--/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies)
-- takes its surface and its text from the one --bt-user pair: a skin that repaints
-- the host's bubble surface alone would otherwise leave the text unreadable. */
--[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important}
--[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit}
--/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */
--[class*="_userRow"] [class*="_actions"]{display:none!important}
--[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start}
--[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none}
--[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible}
--[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none}
--[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)}
--[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)}
--[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px}
--.bt-q-inline{display:flex;margin:4px 0 6px}
--[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important}
--[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important}
--.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0}
--.bt-q-dock{width:100%;display:flex;justify-content:center}
--.bt-q-dock .bt-q{margin:0 auto 8px}
--[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important}
--[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important}
--[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}}
--[class*="_toBottomSlot"]>button:active{scale:.9}
--[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important}
--[class*="_toBottomSlot"]>button svg{display:none}
--[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat}
--@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}}
--[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important}
--.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
--@keyframes bt-news-in{from{translate:0 8px;scale:.9}}
--.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in}
--@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}}
--.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none}
--.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px}
--.bt-news-ico svg{width:16px;height:16px}
--.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
--.bt-news-x svg{width:12px;height:12px}
--.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)}
--@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}}
--.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)}
--.bt-open-new{display:flex;flex-direction:column}
--.bt-open-new .bt-new-sep{margin:18px 0 0}
--.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px}
--.bt-earlier{display:flex;flex-direction:column;gap:6px}
--.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
--.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
--.bt-urow{display:flex;justify-content:flex-end}
--.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
--.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
--.bt-ububble p{margin:0;line-height:22px}
--.bt-uimages{display:block;font-size:12px;opacity:.7}
--.bt-earlier .bt-event{margin:4px 0}
--.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)}
--.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)}
--`;
--function installStyles(text, tag) {
-- const style = document.createElement("style");
-- style.dataset.dshBot = tag;
-- style.textContent = text;
-- document.head.appendChild(style);
-- return style;
+-function AdminStar() {
+- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 }));
+-}
+-function MainStar() {
+- return (0, import_react2.createElement)(
+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"]);
@@ -5459,7 +3989,9 @@ diff --git a/client.js b/client.js
+var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon };
+function FlyBotMark({ size = 24 }) {
+ return (0, import_react5.createElement)(
-+ "svg",
+ "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" })
+ { className: "bt-flybot-mark", width: size, height: size, viewBox: "0 0 512 512", "aria-hidden": true },
+ (0, import_react5.createElement)(
+ "g",
@@ -5478,19 +4010,49 @@ diff --git a/client.js b/client.js
+ (0, import_react5.createElement)("rect", { className: "bt-flybot-eye", x: 282, y: 222, width: 34, height: 58, rx: 17 }),
+ (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }),
+ (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 })
-+ );
-+}
-+
-+// src/client/questions.js
-+var import_react6 = require("react");
-+var keyLetter = (index) => String.fromCharCode(65 + index);
-+var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
-+var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
-+function useOptionKeys(count, onPick, ownRef) {
-+ const pick2 = (0, import_react6.useRef)(onPick);
-+ pick2.current = onPick;
-+ (0, import_react6.useEffect)(() => {
-+ if (count === 0) return void 0;
+ );
+ }
+-function useSettled(state) {
+- const prev = (0, import_react2.useRef)(state);
+- const [settled, setSettled] = (0, import_react2.useState)(false);
+- (0, import_react2.useEffect)(() => {
+- const was = prev.current;
+- prev.current = state;
+- if (state !== "idle") {
+- setSettled(false);
+- return void 0;
+- }
+- if (was === "idle" || was === "alert") return void 0;
+- setSettled(true);
+- const timer = setTimeout(() => setSettled(false), 1800);
+- return () => clearTimeout(timer);
+- }, [state]);
+- return settled;
+-}
+-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) {
+- const settled = useSettled(state);
+- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size });
+- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null;
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } },
+- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }),
+- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null,
+- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null,
+- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null,
+- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null
+- );
++
++// src/client/questions.js
++var import_react6 = require("react");
++var keyLetter = (index) => String.fromCharCode(65 + index);
++var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
++var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
++function useOptionKeys(count, onPick, ownRef) {
++ const pick2 = (0, import_react6.useRef)(onPick);
++ pick2.current = onPick;
++ (0, import_react6.useEffect)(() => {
++ if (count === 0) return void 0;
+ const onKey = (event) => {
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return;
+ if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return;
@@ -5507,7 +4069,14 @@ diff --git a/client.js b/client.js
+ document.addEventListener("keydown", onKey, true);
+ return () => document.removeEventListener("keydown", onKey, true);
+ }, [count, ownRef]);
-+}
+ }
+-function clusterSeats(count, frame) {
+- if (count === 1) return [[0, 0, frame]];
+- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]];
+- if (count === 3) {
+- const size = frame * 5 / 9;
+- const rest = frame - size;
+- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]];
+function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) {
+ const [own, setOwn] = (0, import_react6.useState)("");
+ const [picked, setPicked] = (0, import_react6.useState)([]);
@@ -5576,7 +4145,21 @@ diff --git a/client.js b/client.js
+ ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
+ (0, import_react6.createElement)("span", { className: "bt-q-body" }, field)
+ ));
-+ }
+ }
+- 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
+ let foot = null;
+ if (raised && multi) {
+ foot = (0, import_react6.createElement)(
@@ -5584,10 +4167,23 @@ diff --git a/client.js b/client.js
+ { className: "bt-q-foot" },
+ (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
+ (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
-+ );
+ );
+ } else if ((raised || multi) && canSubmit) {
+ foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
-+ }
+ }
+- 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 (0, import_react6.createElement)(
+ "form",
+ {
@@ -5596,7 +4192,10 @@ diff --git a/client.js b/client.js
+ onSubmit: (event) => {
+ event.preventDefault();
+ submit();
-+ }
+ }
+- const bot = people[index];
+- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
+- })
+ },
+ (0, import_react6.createElement)(
+ "div",
@@ -5618,16 +4217,8 @@ diff --git a/client.js b/client.js
+ !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
+ ) : null,
+ foot
-+ );
+ );
}
--var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g;
--var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g;
--function sheetClassNames(sheet) {
-- const names = [];
-- const visit = (rules) => {
-- for (const rule of rules) {
-- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]);
-- if (rule.cssRules) visit(rule.cssRules);
+function SettledQuestion({ question, answer }) {
+ const options = optionsOf(question);
+ const prompt = question?.question ?? "";
@@ -5715,7 +4306,14 @@ diff --git a/client.js b/client.js
+ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
+ })));
+}
-+
+
+-// 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" }));
+// src/client/secret-card.js
+var import_react7 = require("react");
+var MIN_VALUE = 4;
@@ -5768,9 +4366,16 @@ diff --git a/client.js b/client.js
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
+ event.preventDefault();
+ onEnter?.();
- }
++ }
+ });
-+}
+ }
+-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 ScopeSwitch({ value, onChange, bot }) {
+ const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]];
+ return (0, import_react7.createElement)(
@@ -5785,8 +4390,17 @@ diff --git a/client.js b/client.js
+ className: "bt-secret-seg",
+ onClick: () => onChange(id)
+ }, text))
-+ );
-+}
+ );
+ }
+-function ArrowUpIcon() {
+- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
+-}
+-function MicIcon({ size = 16 }) {
+- return (0, import_react3.createElement)(
+- "svg",
+- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
+- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
+- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
+function SecretHead({ bot, title, detail, name, onClose }) {
+ return (0, import_react7.createElement)(
+ "div",
@@ -5799,8 +4413,13 @@ diff --git a/client.js b/client.js
+ (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
+ ),
+ onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null
-+ );
-+}
+ );
+ }
+-function 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 SecretCard({ bot, request, roster, actions }) {
+ const [value, setValue] = (0, import_react7.useState)("");
+ const [scope, setScope] = (0, import_react7.useState)("all");
@@ -5858,8 +4477,13 @@ diff --git a/client.js b/client.js
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
+ filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
+ )
-+ );
-+}
+ );
+ }
+-function 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 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 });
@@ -5878,11 +4502,30 @@ diff --git a/client.js b/client.js
+ )),
+ canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon))
+ )
-+ );
-+}
+ );
+ }
+-function 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 RequestView({ bot, request, roster, actions }) {
+ return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster });
-+}
+ }
+-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 SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) {
+ const bot = roster.byId[sessionId];
+ if (!bot) return null;
@@ -5892,7 +4535,33 @@ diff --git a/client.js b/client.js
+ return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
+ }
+ return null;
-+}
+ }
+-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 SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
+ const roster = useRoster((value) => value);
+ const shown = useCards?.((state) => state[sessionId]) ?? NONE;
@@ -5903,7 +4572,47 @@ diff --git a/client.js b/client.js
+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry));
+ if (waiting.length === 0) return null;
+ return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
-+}
+ }
+-var 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]);
+var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some";
+function ScopeEditor({ secret, roster, onSave }) {
+ const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope));
@@ -5938,7 +4647,22 @@ diff --git a/client.js b/client.js
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access"))
+ )
+ );
-+}
+ }
+-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] });
+function ValueEditor({ secret, onSave }) {
+ const [value, setValue] = (0, import_react7.useState)("");
+ const [replaced, setReplaced] = (0, import_react7.useState)(false);
@@ -5952,15 +4676,35 @@ diff --git a/client.js b/client.js
+ }
+ });
};
-- try {
-- visit(sheet.cssRules);
-- } catch {
-- }
-- return names;
+- const submit = () => {
+- if (!canSubmit) return;
+- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} });
+ const edit = (next) => {
+ setValue(next);
+ setReplaced(false);
-+ };
+ };
+- 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)(
+ return (0, import_react7.createElement)(
+ "div",
+ { className: "bt-secret-edit-block" },
@@ -5973,17 +4717,7 @@ diff --git a/client.js b/client.js
+ ),
+ value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null
+ );
- }
--function compileFragmentSelectors(text, names) {
-- const compiled = /* @__PURE__ */ new Map();
-- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => {
-- if (!compiled.has(fragment)) {
-- const exact = [];
-- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`);
-- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole);
-- }
-- return compiled.get(fragment);
-- });
++}
+function SecretRow({ secret, roster, actions, run, open, onToggle }) {
+ const [confirm, setConfirm] = (0, import_react7.useState)(false);
+ (0, import_react7.useEffect)(() => {
@@ -5996,22 +4730,85 @@ diff --git a/client.js b/client.js
+ "div",
+ { className: "bt-secret-item", "data-open": open ? "" : void 0 },
+ (0, import_react7.createElement)(
-+ "button",
+ "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)(
+ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
+ (0, import_react7.createElement)(
-+ "span",
+ "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)(
+- "div",
+- { className: "bt-q-head" },
+- (0, import_react4.createElement)(
+- "div",
+- { className: "bt-q-text" },
+- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
+- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
+- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
+ { className: "bt-set-copy" },
+ (0, import_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
+ (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta)
-+ ),
+ ),
+- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon))
+ (0, import_react7.createElement)(
+ "span",
+ { className: "bt-set-control" },
+ secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
+ )
-+ ),
+ ),
+- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
+- field && !raised ? (0, import_react4.createElement)(
+ open ? (0, import_react7.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-secret-edit" },
+ (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
+ (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
@@ -6025,19 +4822,14 @@ diff --git a/client.js b/client.js
+ ] : (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete"))
+ )
+ ) : null
-+ );
+ );
}
--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);
+-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 AddSecret({ actions, run, onDone }) {
+ const [name, setName] = (0, import_react7.useState)("");
+ const [purpose, setPurpose] = (0, import_react7.useState)("");
@@ -6049,18 +4841,9 @@ diff --git a/client.js b/client.js
+ if (ok) {
+ setValue("");
+ onDone();
- }
-- for (const name of list) names.add(name);
-- }
-- const next = compileFragmentSelectors(text, names);
-- if (style.textContent !== next) style.textContent = next;
++ }
+ });
- };
-- render();
-- let timer;
-- const schedule = () => {
-- clearTimeout(timer);
-- timer = setTimeout(render, 120);
++ };
+ return (0, import_react7.createElement)(
+ "div",
+ { className: "bt-set-row bt-set-paste bt-secret-add" },
@@ -6068,7 +4851,14 @@ diff --git a/client.js b/client.js
+ (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }),
+ (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })),
+ (0, import_react7.createElement)(
-+ "div",
+ "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-secret-row-actions" },
+ (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")),
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
@@ -6177,9 +4967,7 @@ diff --git a/client.js b/client.js
+ bot: local ? local.name : remoteBotName(p, "Bot"),
+ mine: Boolean(local)
+ };
- };
-- const onLoad = (event) => {
-- if (event.target instanceof HTMLLinkElement) schedule();
++ };
+ const seats = records.filter((p) => p.leftSeq === void 0).map(seat);
+ for (const id of room2.members ?? []) {
+ const bot = roster.byId[id];
@@ -6271,15 +5059,7 @@ diff --git a/client.js b/client.js
+ } finally {
+ setBusy(false);
+ }
- };
-- 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 (!waiting) {
+ const failed = held.status === "failed";
+ return (0, import_react8.createElement)(
@@ -6290,10 +5070,16 @@ diff --git a/client.js b/client.js
+ // different field elsewhere. A failure that can never succeed draws no button on any surface.
+ failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
+ error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
-+ );
-+ }
+ );
+ }
+- return (0, import_react4.createElement)(
+ const plain = (0, import_react8.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)(
+ { className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") },
+ (0, import_react8.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")),
+ (0, import_react8.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
@@ -6334,7 +5120,7 @@ diff --git a/client.js b/client.js
+ event.preventDefault();
+ event.stopPropagation();
+ actions.openSession(target);
- };
++ };
+ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null;
+ return (0, import_react9.createElement)(
+ "div",
@@ -6345,7 +5131,11 @@ diff --git a/client.js b/client.js
+ const last = index === texts2.length - 1;
+ const live = streaming && last;
+ return (0, import_react9.createElement)(
-+ "div",
+ "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))
+ { key: index, className: "bt-msg" },
+ (0, import_react9.createElement)(
+ "div",
@@ -6365,33 +5155,23 @@ diff --git a/client.js b/client.js
+ "aria-label": t("Remove reaction {emoji}", { emoji }),
+ onClick: () => actions.toggleReaction(msgKey, index, emoji)
+ }, 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 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);
+var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval";
+var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"];
+function leaveNoticeCoveredByClose(room2, text) {
@@ -6412,56 +5192,21 @@ diff --git a/client.js b/client.js
+ 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 MessageTools({ text, name, sessionId, react, actions }) {
+ const [panel, setPanel] = (0, import_react9.useState)(null);
+ const [place, setPlace] = (0, import_react9.useState)(null);
@@ -6473,34 +5218,46 @@ diff --git a/client.js b/client.js
+ const open = (kind, event) => {
+ if (panel === kind) {
+ close();
-+ return;
-+ }
+ return;
+ }
+- pending.answer({ answers: next });
+ 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) } }));
+- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+-}
+-function PendingQuestion({ sessionId, question, actions, inline }) {
+- const [answered, setAnswered] = (0, import_react4.useState)(null);
+- if (answered === question.id) return null;
+- const onAnswer = (answer) => {
+- setAnswered(question.id);
+- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null));
+ const copy2 = () => {
+ void navigator.clipboard?.writeText(text).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1e3);
+ });
-+ };
-+ const download2 = () => {
+ };
+- const onDismiss = () => {
+- setAnswered(question.id);
+- void actions.dismissQuestion(sessionId);
++ 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);
-+ };
+ };
+- return (0, import_react4.createElement)(
+ const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
+ close();
+ run();
+ } }, (0, import_react9.createElement)(Icon), label);
+ return (0, import_react9.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 })
+ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
+ react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null,
+ sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react9.createElement)(ReplyIcon)) : null,
@@ -6519,13 +5276,14 @@ diff --git a/client.js b/client.js
+ item(t("Copy"), CopyIcon, copy2),
+ item(t("Download"), DownloadIcon, download2)
+ ), document.body) : null
-+ );
+ );
}
--function StarIcon({ size = 8 }) {
-- return (0, import_react2.createElement)(
-- "svg",
-- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true },
-- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" })
+-function 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 ThinkingRow({ text }) {
+ if (typeof text !== "string" || text.trim() === "") return null;
+ return (0, import_react9.createElement)(
@@ -6533,10 +5291,20 @@ diff --git a/client.js b/client.js
+ { className: "bt-think" },
+ (0, import_react9.createElement)("summary", null, (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), t("Thought it through")),
+ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text)
- );
++ );
}
--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 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 useFileMentions({ node, openFile, fileMentions, useTurnData }) {
+ const tail = useTurnData?.("turn-tail");
+ const location2 = node.location;
@@ -6547,11 +5315,60 @@ diff --git a/client.js b/client.js
+ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0;
+ }, [fileMentions, openFile, seq, tail, turn]);
}
--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" })
+-
+-// 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 useClosingText(node, useTurnData) {
+ const tail = useTurnData?.("turn-tail");
+ const seq = node.data.finalNode?.seq;
@@ -6600,24 +5417,8 @@ diff --git a/client.js b/client.js
+ }),
+ reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null,
+ held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
- );
- }
--function useSettled(state) {
-- const prev = (0, import_react2.useRef)(state);
-- const [settled, setSettled] = (0, import_react2.useState)(false);
-- (0, import_react2.useEffect)(() => {
-- const was = prev.current;
-- prev.current = state;
-- if (state !== "idle") {
-- setSettled(false);
-- return void 0;
-- }
-- if (was === "idle" || was === "alert") return void 0;
-- setSettled(true);
-- const timer = setTimeout(() => setSettled(false), 1800);
-- return () => clearTimeout(timer);
-- }, [state]);
-- return settled;
++ );
++}
+function HeldReply({ held, roster, sessionId, actions, closing, children }) {
+ const bot = roster.byId[sessionId];
+ const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to);
@@ -6628,19 +5429,7 @@ diff --git a/client.js b/client.js
+ children,
+ closing ? (0, import_react9.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
+ );
- }
--function 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 PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) {
+ const lead = (0, import_react9.createElement)("span", { className: "bt-lead", title: name }, (0, import_react9.createElement)(PersonMark, { name }));
+ return (0, import_react9.createElement)(
@@ -6648,15 +5437,8 @@ diff --git a/client.js b/client.js
+ { className: "bt-group-msg bt-person-msg", "data-person": name },
+ (0, import_react9.createElement)("span", { className: "bt-group-name" }, name),
+ (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead })
- );
- }
--function clusterSeats(count, frame) {
-- if (count === 1) return [[0, 0, frame]];
-- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]];
-- if (count === 3) {
-- const size = frame * 5 / 9;
-- const rest = frame - size;
-- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]];
++ );
++}
+function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
+ if (whole && bots.length === 1) {
+ return (0, import_react9.createElement)(
@@ -6670,9 +5452,7 @@ diff --git a/client.js b/client.js
+ (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name)
+ )
+ );
- }
-- const half = frame / 2;
-- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
++ }
+ const parts = [];
+ parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix));
+ bots.forEach((bot, index) => {
@@ -6697,19 +5477,11 @@ diff --git a/client.js b/client.js
+ 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 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 ScopeSwitch({ value, onChange, bot }) {
+- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
+- return (0, import_react5.createElement)(
+var HELD_SENDING_MS = 6e4;
+function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) {
+ if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0;
@@ -6744,12 +5516,28 @@ diff --git a/client.js b/client.js
+ room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
+ );
+ return (0, import_react9.createElement)(
-+ "div",
+ "div",
+- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
+- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
+- options.map(([id, text]) => (0, import_react5.createElement)("button", {
+- key: id,
+- type: "button",
+- role: "radio",
+- "aria-checked": value === id,
+- className: "bt-secret-seg",
+- onClick: () => onChange(id)
+- }, text))
+ { className: "bt-group-msg" },
+ name,
+ (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
-+ );
-+}
+ );
+ }
+-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 remoteSpeaker(source, roster, sessionId) {
+ const handle = source.remote.handle;
+ const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle);
@@ -6790,7 +5578,10 @@ diff --git a/client.js b/client.js
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ if (person) {
+ return (0, import_react9.createElement)(
-+ "div",
+ "div",
+- { className: "bt-q-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}` : "")
+ { className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle },
+ // One short header: the person, and the Bot that carried the line only when it is known (#275).
+ (0, import_react9.createElement)(
@@ -6801,25 +5592,8 @@ diff --git a/client.js b/client.js
+ ),
+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }),
+ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
- );
- }
-- const seats = clusterSeats(Math.min(people.length, 4), size);
-- const overflow = people.length > 4 ? people.length - 3 : 0;
-- const gap = size / 14;
-- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap }));
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-cluster", style: { width: size, height: size } },
-- seats.map(([x, y, s], index) => {
-- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`);
-- const style = { left: x, top: y, width: s, height: s };
-- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" });
-- if (overflow > 0 && index === 3) {
-- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`);
-- }
-- const bot = people[index];
-- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
-- })
++ );
++ }
+ const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 });
+ const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark);
+ return (0, import_react9.createElement)(
@@ -6831,54 +5605,48 @@ diff --git a/client.js b/client.js
+ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
+ name,
+ (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle)
-+ ),
+ ),
+- 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
+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }),
+ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
);
}
--
--// 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 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);
+function FirstContact({ source, roster, sessionId }) {
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ const who = person || remoteSpeaker(source, roster).name;
+ const bot = roster.byId[sessionId]?.name ?? t("your Bot");
+ return (0, import_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot })));
- }
--function PeopleIcon() {
-- return (0, import_react3.createElement)(
++}
+function GlobeMark() {
+ return (0, import_react9.createElement)(
- "svg",
-- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true },
-- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
-- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
-- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
++ "svg",
+ { width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
+ (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
+ (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
- );
- }
--function ArrowUpIcon() {
-- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
--}
--function MicIcon({ size = 16 }) {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
-- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
-- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
-+function RemoteFiles({ files, plugin, botId }) {
++ );
++}
++function RemoteFiles({ files, plugin, botId }) {
+ const [busy, setBusy] = (0, import_react9.useState)("");
+ const [error, setError] = (0, import_react9.useState)("");
+ const download2 = async (file) => {
+ setBusy(file.id);
-+ setError("");
+ 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 }));
+ try {
+ const got = await call(`flybot/${plugin}/attachment`, { id: file.id, botId });
+ const value = got?.value ?? got;
@@ -6892,7 +5660,9 @@ diff --git a/client.js b/client.js
+ } finally {
+ setBusy("");
+ }
-+ };
+ };
+- const allow = () => {
+- if (!busy) void run(actions.secretAllow(request.id));
+ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-file-chips" },
@@ -6902,26 +5672,16 @@ diff --git a/client.js b/client.js
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
+ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
- );
- }
--function WaveIcon({ size = 16 }) {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
-- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
++ );
++}
+function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-event" },
+ (0, import_react9.createElement)("span", null, prefix),
+ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null
- );
- }
--function ShareIcon() {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
++ );
++}
+function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
+ if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null;
+ const self = roster.byId[sessionId];
@@ -6934,123 +5694,17 @@ diff --git a/client.js b/client.js
+ "div",
+ { className: "bt-event" },
+ (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label)
- );
- }
--function CloseIcon() {
-- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++ );
++}
+function PartLine({ time }) {
+ return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null);
- }
--var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" }));
--var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }));
--var lineIcon = (paths) => function LineIcon() {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d }))
-- );
++}
+var scrollParent = (node) => {
+ for (let at = node.parentElement; at; at = at.parentElement) {
+ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at;
+ }
+ return null;
- };
--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;
-- }
-- onAnswer({ selected: [label] });
-- };
-- const submit = () => {
-- if (!canSubmit) return;
-- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} });
++};
+function EarlierParts({ sessionId, roster, actions }) {
+ const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
+ const top = (0, import_react9.useRef)(null);
@@ -7072,77 +5726,9 @@ diff --git a/client.js b/client.js
+ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
+ });
};
-- 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)(
+- 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)(
+ (0, import_react9.useEffect)(() => {
+ alive.current = true;
+ load(void 0);
@@ -7172,30 +5758,43 @@ diff --git a/client.js b/client.js
+ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item);
+ }
+ return (0, import_react9.createElement)(
-+ "div",
+ "div",
+- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind },
+- (0, import_react5.createElement)(SecretHead, {
+- bot,
+- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }),
+- detail: request.purpose,
+- name: request.name,
+- onClose: busy ? void 0 : cancel
+- }),
+- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null,
+- filling ? (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-body" },
+- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })),
+- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null
+- ) : null,
+- (0, import_react5.createElement)(
+ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading },
+ (0, import_react9.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))
+- { className: "bt-secret-note", id: noteId },
+- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }),
+- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`),
+- ".",
+- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : ""
+ { ref: top, className: "bt-earlier-top" },
+ view.loading ? (0, import_react9.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null
),
-- rows.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
+- warn,
+- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null,
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-q-foot" },
+- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
+- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
+- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
+- )
+ rows.map((row, index) => {
+ const key = rows.length - index;
+ switch (row.kind) {
@@ -7227,21 +5826,25 @@ diff --git a/client.js b/client.js
+ (0, import_react9.createElement)(PartLine, { time: view.startedAt })
);
}
--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 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 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-q-head" },
+- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })),
+- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)(
+- "p",
+- { className: "bt-q-title" },
+- text,
+- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
+- )),
+- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon))
+- )
+- );
+function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
+ const roster = useRoster((value) => value);
+ const data = node.data;
@@ -7320,59 +5923,13 @@ diff --git a/client.js b/client.js
+ }
+ if (source.kind === "user-question-reply") return null;
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
- }
-- return (0, import_react4.createElement)(
-- "div",
-- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
-- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
-- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
-- const optionIndex = options.findIndex((option) => option.label === label);
-- return (0, import_react4.createElement)(
-- "div",
-- { key: `${label}-${index}`, className: "bt-q-opt" },
-- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
-- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)),
-- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon))
-- );
-- }))
-- );
- }
--function QuestionCard({ matched }) {
-- const pending = matched;
-- const questions = pending.questions ?? [];
-- const [index, setIndex] = (0, import_react4.useState)(0);
-- const [answers, setAnswers] = (0, import_react4.useState)([]);
-- const question = questions[index];
-- if (question === void 0) return null;
-- const onAnswer = (answer) => {
-- const next = [...answers, { id: question.id, ...answer }];
-- if (index + 1 < questions.length) {
-- setAnswers(next);
-- setIndex(index + 1);
-- return;
-- }
-- pending.answer({ answers: next });
-- };
-- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
++ }
++}
+function NewDivider() {
+ return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New")));
}
--function PendingQuestion({ sessionId, question, actions, inline }) {
-- const [answered, setAnswered] = (0, import_react4.useState)(null);
-- if (answered === question.id) return null;
-- const onAnswer = (answer) => {
-- setAnswered(question.id);
-- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null));
-- };
-- const onDismiss = () => {
-- setAnswered(question.id);
-- void actions.dismissQuestion(sessionId);
-- };
-- return (0, import_react4.createElement)(
-- "div",
-- { className: inline ? "bt-q-inline" : "bt-q-dock" },
-- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
-- );
+-function 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 TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
+ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
+ (0, import_react9.useEffect)(() => {
@@ -7391,20 +5948,27 @@ diff --git a/client.js b/client.js
+ });
+ return fresh ? (0, import_react9.createElement)(NewDivider) : null;
}
--function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
-- const question = useRoster((value) => value.questions?.[sessionId]);
-- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true;
-- const loading = useSession?.(historyLoading) === true;
-- if (question === void 0 || question.callId && (drawn || loading)) return null;
-- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
+-function 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 Nothing() {
-+ return null;
+ return null;
}
--function AnsweredCell({ node }) {
-- const data = node.data ?? {};
-- const questions = Array.isArray(data.questions) ? data.questions : [];
-- if (questions.length === 0) {
-- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
+-function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
+- const roster = useRoster((value) => value);
+- const shown = useCards?.((state) => state[sessionId]) ?? NONE;
+- const loading = useSession?.(historyLoading) === true;
+- const bot = roster.byId[sessionId];
+- if (!bot || currentPart(bot) !== sessionId) return null;
+- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0);
+- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry));
+- if (waiting.length === 0) return null;
+- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
+var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]);
+var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name);
+var SILENT_REGISTRATION_TOOLS = /* @__PURE__ */ new Set([
@@ -7453,71 +6017,42 @@ diff --git a/client.js b/client.js
+ if (isHiddenNotice(block)) continue;
+ if (block?.type === "tool-removal" && block.toolName) parts.push(`Tool removed: ${block.toolName}`);
+ else if (block?.type === "tool-addition" && block.toolName) parts.push(`Tool added: ${block.toolName}`);
- }
-- const answers = Array.isArray(data.answers) ? data.answers : [];
-- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
-- key: question.id,
-- question,
-- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
-- })));
++ }
+ if (parts.length === 0) return null;
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · "));
}
--
--// 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 ScopeEditor({ secret, roster, onSave }) {
+- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some");
+- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope);
+- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
+- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join();
+- return (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-edit-block" },
+- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) },
+- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)(
+- "label",
+- { key: id, className: "bt-secret-radio" },
+- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
+- text
+- ))
+- ),
+- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", {
+- key: bot.id,
+- type: "button",
+- className: "bt-secret-bot",
+- "aria-pressed": picked.includes(bot.id),
+- onClick: () => toggle(bot.id)
+- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null,
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-row-actions" },
+- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access"))
+- )
+- );
+var SEPARATOR_GAP_MS = 30 * 60 * 1e3;
+function clockLabel(time, now = Date.now()) {
+ const date = new Date(time);
@@ -7529,22 +6064,36 @@ 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 ScopeSwitch({ value, onChange, bot }) {
-- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
+-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-scope", role: "radiogroup", "aria-label": t("Who may use it") },
-- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
-- options.map(([id, text]) => (0, import_react5.createElement)("button", {
-- key: id,
-- type: "button",
-- role: "radio",
-- "aria-checked": value === id,
-- className: "bt-secret-seg",
-- onClick: () => onChange(id)
-- }, text))
-- );
-+function nextTurnStart(turns, turn) {
+- { className: "bt-secret-edit-block" },
+- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-inline" },
+- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
+- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
+- ),
+- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null
+- );
++function nextTurnStart(turns, turn) {
+ let next;
+ for (const key in turns) {
+ const candidate = Number(key);
@@ -7552,18 +6101,47 @@ diff --git a/client.js b/client.js
+ }
+ return next === void 0 ? void 0 : turns[next].first;
}
--function SecretHead({ bot, title, detail, name, onClose }) {
+-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-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))),
+- { className: "bt-secret-item", "data-open": open ? "" : void 0 },
- (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}` : "")
+- "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 })))
+- )
- ),
-- 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
+- open ? (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-edit" },
+- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
+- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-secret-row-actions" },
+- confirm ? [
+- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
+- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
+- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete"))
+- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete"))
+- )
+- ) : null
- );
+function openingTurn(turns, firstTurns, hiddenTurns) {
+ if (!turns || !firstTurns) return null;
@@ -7574,64 +6152,33 @@ 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 SecretCard({ bot, request, roster, actions }) {
+-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 [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 ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && 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));
+- if (!ready) return;
+- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => {
+- if (ok) {
+- setValue("");
+- onDone();
+- }
+- });
- };
-- 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,
+- { 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-q-foot" },
-- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
-- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
+- { className: "bt-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"))
- )
-- );
+var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
+function movedOnSince(visit, turns, hiddenTurns) {
+ if (typeof visit?.entered !== "number") return false;
@@ -7640,25 +6187,7 @@ diff --git a/client.js b/client.js
+ if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true;
+ }
+ return false;
- }
--function SettledSecret({ bot, request, roster }) {
-- const canceled = request.status === "canceled";
-- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name });
-- return (0, import_react5.createElement)(
-- "div",
-- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status },
-- (0, import_react5.createElement)(
-- "div",
-- { className: "bt-q-head" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })),
-- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)(
-- "p",
-- { className: "bt-q-title" },
-- text,
-- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
-- )),
-- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon))
-- )
++}
+var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit;
+function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
+ const turn = location2.turn;
@@ -7684,18 +6213,32 @@ diff --git a/client.js b/client.js
+ fresh ? (0, import_react9.createElement)(NewDivider) : null
);
}
--function RequestView({ bot, request, roster, actions }) {
-- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster });
+-function SecretsPage({ roster, actions, run, Section }) {
+- const [open, setOpen] = (0, import_react5.useState)(null);
+- const [adding, setAdding] = (0, import_react5.useState)(false);
+- const secrets = roster.secrets ?? [];
+- return [
+- (0, import_react5.createElement)(
+- "p",
+- { key: "about", className: "bt-secret-about" },
+- t("Keys your Bots asked for. Bots never see the values: they use them as "),
+- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"),
+- t(" in their shell, and every value is replaced by "),
+- (0, import_react5.createElement)("code", null, "[secret:NAME]"),
+- t(" before anything goes to a model.")
+- ),
+- (0, import_react5.createElement)(
+- Section,
+- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
+- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
+- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
+- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
+- ),
+- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key")))
+- ];
+function Shimmer({ children }) {
+ return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children);
- }
--function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) {
-- const bot = roster.byId[sessionId];
-- if (!bot) return null;
-- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId);
-- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions }));
-- if (done && /already saved/.test(contentText(content))) {
-- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
++}
+var PHRASES = {
+ thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"],
+ working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"]
@@ -7752,100 +6295,248 @@ diff --git a/client.js b/client.js
+ if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" };
+ const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1];
+ return { label: phrase ? t(phrase) : null, state: toolState(name) };
- }
-- return null;
- }
--function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
-- const roster = useRoster((value) => value);
-- const shown = useCards?.((state) => state[sessionId]) ?? NONE;
-- const loading = useSession?.(historyLoading) === true;
-- const bot = roster.byId[sessionId];
-- if (!bot || currentPart(bot) !== sessionId) return null;
-- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0);
-- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry));
-- if (waiting.length === 0) return null;
-- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
-+function workPhrase(tool) {
-+ if (!tool) return t("Thinking about it");
-+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool });
- }
--function ScopeEditor({ secret, roster, onSave }) {
-- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some");
-- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope);
-- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
-- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join();
-- return (0, import_react5.createElement)(
-+function workText(tool, step) {
-+ return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`;
-+}
-+function useLiveNote(actions, sessionId, id, entry) {
-+ const key = entry ? JSON.stringify(entry) : "";
-+ (0, import_react9.useEffect)(() => {
-+ if (!key) return void 0;
-+ actions.noteLive(sessionId, id, JSON.parse(key));
-+ return () => actions.noteLive(sessionId, id, null);
-+ }, [sessionId, id, key]);
-+}
-+function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) {
-+ const [live, setLive] = (0, import_react9.useState)({ replies: [] });
-+ (0, import_react9.useEffect)(() => {
-+ if (done) return void 0;
-+ let stop = false;
-+ let timer;
-+ let seen = "";
-+ const tick = async () => {
-+ const result = await actions.roomProgress(sessionId);
-+ if (stop) return;
-+ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
-+ const key = entry ? JSON.stringify(entry.replies) : "";
-+ if (entry && key !== seen) {
-+ seen = key;
-+ setLive({ replies: entry.replies ?? [] });
-+ }
-+ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
-+ };
-+ void tick();
-+ return () => {
-+ stop = true;
-+ clearTimeout(timer);
-+ };
-+ }, [done, sessionId, callId]);
-+ let replies = live.replies;
-+ if (done) {
-+ try {
-+ replies = JSON.parse(contentText(content)).replies ?? [];
-+ } catch {
-+ replies = [];
-+ }
+ }
-+ if (replies.length === 0) return null;
-+ const room2 = roster.roomsById[sessionId];
-+ return (0, import_react9.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)(
+ }
+-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 });
+ }
+-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
+- );
+- })
+- );
++function workText(tool, step) {
++ return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`;
+ }
+-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 useLiveNote(actions, sessionId, id, entry) {
++ const key = entry ? JSON.stringify(entry) : "";
++ (0, import_react9.useEffect)(() => {
++ if (!key) return void 0;
++ actions.noteLive(sessionId, id, JSON.parse(key));
++ return () => actions.noteLive(sessionId, id, null);
++ }, [sessionId, id, key]);
++}
++function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) {
++ const [live, setLive] = (0, import_react9.useState)({ replies: [] });
++ (0, import_react9.useEffect)(() => {
++ if (done) return void 0;
++ let stop = false;
++ let timer;
++ let seen = "";
++ const tick = async () => {
++ const result = await actions.roomProgress(sessionId);
++ if (stop) return;
++ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
++ const key = entry ? JSON.stringify(entry.replies) : "";
++ if (entry && key !== seen) {
++ seen = key;
++ setLive({ replies: entry.replies ?? [] });
+ }
++ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
+ };
+- window.addEventListener("mousedown", onDown);
+- window.addEventListener("keydown", onKey, true);
++ void tick();
+ return () => {
+- window.removeEventListener("mousedown", onDown);
+- window.removeEventListener("keydown", onKey, true);
++ stop = true;
++ clearTimeout(timer);
+ };
+- }, [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;
++ }, [done, sessionId, callId]);
++ let replies = live.replies;
++ if (done) {
++ try {
++ replies = JSON.parse(contentText(content)).replies ?? [];
++ } catch {
++ replies = [];
+ }
+- 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)(
++ }
++ if (replies.length === 0) return null;
++ const room2 = roster.roomsById[sessionId];
++ return (0, import_react9.createElement)(
+ "div",
+- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
+- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null,
+- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null,
+- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)),
+- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
- "div",
-- { 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"))
-- )
+- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place },
+- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => {
+- close();
+- react(emoji);
+- } }, emoji))
+- ), document.body) : null,
+- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
+- "div",
+- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place },
+- item(t("Copy"), CopyIcon, copy2),
+- item(t("Download"), DownloadIcon, download2)
+- ), document.body) : null
+ { className: "bt-group-replies" },
+ replies.map((reply, index) => {
+ const stored = roster.byId[reply.botId];
@@ -7928,22 +6619,13 @@ diff --git a/client.js b/client.js
+ })
);
}
--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);
+-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)
+- );
+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) {
@@ -7955,64 +6637,19 @@ diff --git a/client.js b/client.js
+ seen.add(id);
+ calls.push(call4);
+ for (const child of call4.subCalls ?? []) visit(child);
- };
-- return (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-edit-block" },
-- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
-- (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-inline" },
-- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
-- ),
-- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null
-- );
++ };
+ visit(root);
+ return calls;
}
--function SecretRow({ secret, roster, actions, run, open, onToggle }) {
-- const [confirm, setConfirm] = (0, import_react5.useState)(false);
-- (0, import_react5.useEffect)(() => {
-- if (!open) setConfirm(false);
-- }, [open]);
-- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you");
-- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean);
-- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · ");
-- return (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-item", "data-open": open ? "" : void 0 },
-- (0, import_react5.createElement)(
-- "button",
-- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
-- (0, import_react5.createElement)(
-- "span",
-- { className: "bt-set-copy" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
-- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta)
-- ),
-- (0, import_react5.createElement)(
-- "span",
-- { className: "bt-set-control" },
-- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
-- )
-- ),
-- open ? (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-edit" },
-- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
-- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
-- (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-row-actions" },
-- confirm ? [
-- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
-- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
-- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete"))
-- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete"))
-- )
-- ) : null
-- );
+-function 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 useShownCards(actions, sessionId, callIds) {
+ const key = JSON.stringify(callIds);
+ (0, import_react9.useLayoutEffect)(() => {
@@ -8022,17 +6659,7 @@ diff --git a/client.js b/client.js
+ return () => actions.noteCards(sessionId, ids, -1);
+ }, [sessionId, key]);
}
--function AddSecret({ actions, run, onDone }) {
-- const [name, setName] = (0, import_react5.useState)("");
-- const [purpose, setPurpose] = (0, import_react5.useState)("");
-- const [value, setValue] = (0, import_react5.useState)("");
-- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE;
-- const save = () => {
-- if (!ready) return;
-- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => {
-- if (ok) {
-- setValue("");
-- onDone();
+-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) {
+function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
+ const tool = toolName(call4);
+ const done = call4.kind === "tool-result";
@@ -8092,23 +6719,7 @@ diff --git a/client.js b/client.js
+ parsed = JSON.parse(contentText(call4.content));
+ } catch {
+ parsed = null;
- }
-- });
-- };
-- 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"))
-- )
-- );
++ }
+ const answer = parsed?.answers?.[0];
+ const question = argValue("questions")?.[0];
+ if (!answer) return null;
@@ -8125,32 +6736,41 @@ diff --git a/client.js b/client.js
+ default:
+ return null;
+ }
- }
--function SecretsPage({ roster, actions, run, Section }) {
-- const [open, setOpen] = (0, import_react5.useState)(null);
-- const [adding, setAdding] = (0, import_react5.useState)(false);
-- const secrets = roster.secrets ?? [];
-- return [
-- (0, import_react5.createElement)(
-- "p",
-- { key: "about", className: "bt-secret-about" },
-- t("Keys your Bots asked for. Bots never see the values: they use them as "),
-- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"),
-- t(" in their shell, and every value is replaced by "),
-- (0, import_react5.createElement)("code", null, "[secret:NAME]"),
-- t(" before anything goes to a model.")
-- ),
-- (0, import_react5.createElement)(
-- Section,
-- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
-- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
-- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
-- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
-- ),
-- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key")))
-- ];
++}
+function 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);
@@ -8165,90 +6785,95 @@ diff --git a/client.js b/client.js
+ const env = { sessionId, roster, actions, useUi, findBot };
+ return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env))));
}
--var SECRET_CSS = `
--.bt-secret-head{align-items:center;gap:10px}
--.bt-secret-who{position:relative;flex:none;display:inline-flex}
--.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)}
--.bt-secret-lock svg{width:11px;height:11px}
--.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)}
--.bt-secret-body{display:flex;flex-direction:column;gap:8px}
--.bt-secret-field{align-items:center;min-height:36px}
--.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em}
--.bt-secret-input[data-masked]{-webkit-text-security:disc}
--.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0}
--.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)}
--.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}}
--.bt-secret-glide[data-at="1"]{transform:translateX(100%)}
--.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease}
--.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500}
--.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
--/* A key card asks the user to act, so the process group carrying it stays open in every
-- work-details mode (same override as .bt-group-replies in styles.js). */
--[data-step-process]:has(.bt-secret)>div:first-child{display:none!important}
--[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
--.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)}
--.bt-secret-ok svg{width:13px;height:13px;color:#16a34a}
--@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}}
--.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
--.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)}
--.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}
--.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)}
--.bt-secret-error{font-size:12px;line-height:16px;color:#e02135}
--.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default}
--.bt-secret-done .bt-q-head{align-items:center}
--.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500}
--.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)}
--.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
--.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards}
--@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}}
--@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}}
--.bt-secret-event{display:inline-flex;align-items:center;gap:5px}
--.bt-secret-dock{flex-direction:column;align-items:center}
--.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))}
--.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)}
--.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)}
--.bt-secret-title svg{flex:none;color:var(--bt-ink-2)}
--.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)}
--.bt-secret-faces{display:inline-flex}
--.bt-secret-faces>*+*{margin-left:-6px}
--.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
--.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px}
--.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)}
--.bt-secret-radios{display:flex;gap:16px;font-size:13px}
--.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer}
--.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px}
--.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease}
--.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)}
--.bt-secret-bot svg{width:13px;height:13px}
--.bt-secret-inline{display:flex;gap:8px;align-items:center}
--.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
--.bt-secret-row-actions .bt-set-hint{flex:1}
--.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px}
--.bt-secret-danger svg{width:13px;height:13px}
--.bt-secret-add .bt-input{height:34px}
--@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
--`;
-
--// src/client/turn-reasoning.js
--function turnReasoning(steps, current) {
-- const members = steps.length ? steps : [current];
-- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
-- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
-+// src/client/awaiting.js
-+var AWAIT_REPLY_MS = 10 * 60 * 1e3;
-+function awaitingReply(roster, sessionId, now = Date.now()) {
-+ const at = roster?.remoteAwaiting?.[sessionId];
-+ if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false;
-+ return now - at < AWAIT_REPLY_MS;
-+}
-+
-+// src/client/remote-approval-wait.js
-+var REMOTE_APPROVAL_LABEL = "Waiting for their approval";
-+function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
-+ if (!remote) return null;
+-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
+- if (whole && bots.length === 1) {
+- return (0, import_react6.createElement)(
+- "div",
+- { className: "bt-event" },
+- (0, import_react6.createElement)(
+- "button",
+- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
+- (0, import_react6.createElement)("span", null, prefix),
+- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }),
+- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name)
+- )
+- );
+- }
+- const parts = [];
+- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
+- bots.forEach((bot, index) => {
+- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner));
+- parts.push((0, import_react6.createElement)(
+- "button",
+- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
+- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
+- bot.name
+- ));
+- });
+- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts);
++
++// src/client/awaiting.js
++var AWAIT_REPLY_MS = 10 * 60 * 1e3;
++function awaitingReply(roster, sessionId, now = Date.now()) {
++ const at = roster?.remoteAwaiting?.[sessionId];
++ if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false;
++ return now - at < AWAIT_REPLY_MS;
+ }
+-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) {
+- const lead = (0, import_react6.createElement)(
+- "button",
+- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) },
+- (0, import_react6.createElement)(BotMark, { bot, size: 22 })
+- );
+- return (0, import_react6.createElement)(
+- "div",
+- { className: "bt-group-msg" },
+- (0, import_react6.createElement)(
+- "span",
+- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
+- bot.name,
+- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
+- ),
+- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
+- );
++
++// src/client/remote-approval-wait.js
++var REMOTE_APPROVAL_LABEL = "Waiting for their approval";
++function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
++ if (!remote) return null;
+ if (tool && inputAt <= (tool.at ?? 0)) return null;
+ return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" };
-+}
+ }
+-function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+- return (0, import_react6.createElement)(
+- "div",
+- { className: "bt-event" },
+- (0, import_react6.createElement)("span", null, prefix),
+- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null
+- );
++
++// src/client/activity-row-host.js
++var RUNNING_HOST = "[data-chat-flow] [data-chat-running]";
++var TAIL_HOST = "tail";
++function findRunningHostIn(root) {
++ if (!root || typeof root.querySelector !== "function") return null;
++ if (typeof root.matches === "function" && root.matches("[data-chat-running]")) return root;
++ if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]");
++ return root.querySelector(RUNNING_HOST);
+ }
+-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_react10 = require("react");
@@ -8261,58 +6886,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`;
}
--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 RUNNING_HOST = "[data-chat-flow] > [data-chat-running]";
-+var TAIL_HOST = "tail";
+-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];
@@ -8325,24 +6900,16 @@ diff --git a/client.js b/client.js
+ }, [shown, at]);
+ return shown;
}
--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();
-- }
+-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 document.querySelector(where);
++ if (where !== TAIL_HOST) return findRunningHostIn(document);
+ const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]");
+ if (!flow) return null;
-+ const running = flow.querySelector(":scope > [data-chat-running]");
++ const running = findRunningHostIn(flow);
+ if (running) return running;
+ const tails = flow.querySelectorAll(":scope > * [data-turn-tail], :scope > [data-turn-tail]");
+ return tails.length ? tails[tails.length - 1] : null;
@@ -8351,7 +6918,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_react10.useRef)(-1);
+ const wasSending = (0, import_react10.useRef)(false);
@@ -8373,52 +6959,26 @@ diff --git a/client.js b/client.js
+ const check = () => {
+ frame = 0;
+ if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true);
- };
-- window.addEventListener("mousedown", onDown);
-- window.addEventListener("keydown", onKey, true);
++ };
+ const observer = new MutationObserver(() => {
+ if (!frame) frame = requestAnimationFrame(check);
+ });
+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
+ check();
- return () => {
-- window.removeEventListener("mousedown", onDown);
-- window.removeEventListener("keydown", onKey, true);
++ return () => {
+ cancelAnimationFrame(frame);
+ observer.disconnect();
- };
-- }, [open]);
++ };
+ }, [remote, sending]);
+ return replied;
- }
--function MessageTools({ text, name, sessionId, react, actions }) {
-- const [panel, setPanel] = (0, import_react6.useState)(null);
-- const [place, setPlace] = (0, import_react6.useState)(null);
-- const [copied, setCopied] = (0, import_react6.useState)(false);
-- const tools = (0, import_react6.useRef)(null);
-- const pop = (0, import_react6.useRef)(null);
-- const close = () => setPanel(null);
-- useDismiss(panel !== null, close, [tools, pop]);
-- const open = (kind, event) => {
-- if (panel === kind) {
-- close();
-- return;
++}
+function useRunningHost(active, where = RUNNING_HOST) {
+ const [host, setHost] = (0, import_react10.useState)(null);
+ (0, import_react10.useEffect)(() => {
+ if (!active) {
+ setHost(null);
+ return void 0;
- }
-- const rect = event.currentTarget.getBoundingClientRect();
-- const below = rect.bottom + 220 < window.innerHeight;
-- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } });
-- setPanel(kind);
-- };
-- const copy2 = () => {
-- void navigator.clipboard?.writeText(text).then(() => {
-- setCopied(true);
-- setTimeout(() => setCopied(false), 1e3);
++ }
+ let frame = 0;
+ const find = () => {
+ frame = 0;
@@ -8427,57 +6987,7 @@ diff --git a/client.js b/client.js
+ };
+ const observer = new MutationObserver(() => {
+ if (!frame) frame = requestAnimationFrame(find);
- });
-- };
-- 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
-- );
--}
--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 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]);
++ });
+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
+ find();
+ return () => {
@@ -8486,36 +6996,7 @@ diff --git a/client.js b/client.js
+ };
+ }, [active, where]);
+ return host;
- }
--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
++}
+function useRowEntrances(replacing) {
+ const check = (0, import_react10.useRef)(replacing);
+ check.current = replacing;
@@ -8542,264 +7023,29 @@ diff --git a/client.js b/client.js
+ let last = -1;
+ items.forEach((item, index) => {
+ if (seen.has(item)) last = index;
- });
-- }),
-- 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 EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
-- if (whole && bots.length === 1) {
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-event" },
-- (0, import_react6.createElement)(
-- "button",
-- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
-- (0, import_react6.createElement)("span", null, prefix),
-- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }),
-- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name)
-- )
-- );
-- }
-- const parts = [];
-- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
-- bots.forEach((bot, index) => {
-- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner));
-- parts.push((0, import_react6.createElement)(
-- "button",
-- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
-- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
-- bot.name
-- ));
-- });
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts);
--}
--function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) {
-- const lead = (0, import_react6.createElement)(
-- "button",
-- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) },
-- (0, import_react6.createElement)(BotMark, { bot, size: 22 })
-- );
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-group-msg" },
-- (0, import_react6.createElement)(
-- "span",
-- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
-- bot.name,
-- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
-- ),
-- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
-- );
--}
--function 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
-- );
++ });
+ 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 observer = new MutationObserver(() => {
-+ if (!frame) frame = requestAnimationFrame(scan);
-+ });
-+ scan();
-+ return () => {
-+ cancelAnimationFrame(frame);
-+ observer.disconnect();
-+ };
-+ }, []);
- }
--function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
-- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null;
-- const self = roster.byId[sessionId];
-- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim());
-- const other = team || !scope ? void 0 : findBot(scope);
-- const target = other && other.id !== self?.id ? other : self;
-- if (!target) return null;
-- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name });
-- return (0, import_react6.createElement)(
-+function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) {
-+ const roster = useRoster((value) => value);
-+ const running = useSessionStatus((map) => map.get(sessionId)?.running === true);
-+ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {}));
-+ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
-+ const bot = roster.byId[sessionId];
-+ const room2 = roster.roomsById[sessionId];
-+ const entries2 = (0, import_react10.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
-+ const streaming = entries2.some((entry) => entry.text);
-+ const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at);
-+ const relaying = tools.some((entry) => entry.relay);
-+ const awaiting = useAwaitingReply(roster, sessionId);
-+ const sendRun = (0, import_react10.useRef)(false);
-+ if (!running) sendRun.current = false;
-+ else if (pendingInput > 0) sendRun.current = true;
-+ const replied = useReplyAfterSend(bot?.remote !== void 0, running && sendRun.current);
-+ const visible = bot?.remote !== void 0 ? !streaming && (running ? sendRun.current : awaiting && !replied) : running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
-+ const lastRun = (0, import_react10.useRef)({ running, send: false });
-+ const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running;
-+ lastRun.current = { running, send: sendRun.current };
-+ const steady = bridging;
-+ const tool = tools.at(-1);
-+ const inputRef = (0, import_react10.useRef)(0);
-+ const inputAt = Math.max(inputRef.current, pendingInput);
-+ inputRef.current = inputAt;
-+ const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null;
-+ const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
-+ const [shown, setShown] = (0, import_react10.useState)(null);
-+ const wantKey = want?.key ?? null;
-+ const markState = (0, import_react10.useRef)("thinking");
-+ if (want) markState.current = want.state;
-+ (0, import_react10.useEffect)(() => {
-+ if (wantKey === null || shown?.key === wantKey) return void 0;
-+ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() });
-+ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0;
-+ if (wait <= 0) {
-+ make();
-+ return void 0;
-+ }
-+ const timer = setTimeout(make, wait);
-+ return () => clearTimeout(timer);
-+ }, [wantKey, shown]);
-+ const [phase, setPhase] = (0, import_react10.useState)(null);
-+ const endedAt = (0, import_react10.useRef)(0);
-+ const shift = (0, import_react10.useRef)(null);
-+ const leaving = (0, import_react10.useRef)(false);
-+ leaving.current = phase !== null && !visible;
-+ (0, import_react10.useEffect)(() => {
-+ if (visible) {
-+ setPhase("in");
-+ return void 0;
-+ }
-+ let timer;
-+ const leave = () => {
-+ setPhase((current) => current === "in" ? "out" : current);
-+ timer = setTimeout(() => {
-+ shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
-+ setPhase((current) => current === "out" ? null : current);
-+ }, 140);
-+ };
-+ if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS);
-+ else leave();
-+ return () => clearTimeout(timer);
-+ }, [visible]);
-+ (0, import_react10.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_react10.useEffect)(() => {
-+ if (!running) actions.clearLive(sessionId);
-+ }, [running, sessionId]);
-+ useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
-+ const [now, setNow] = (0, import_react10.useState)(Date.now);
-+ (0, import_react10.useEffect)(() => {
-+ if (phase !== "in") return void 0;
-+ const timer = setInterval(() => setNow(Date.now()), 1e4);
-+ return () => clearInterval(timer);
-+ }, [phase]);
-+ const remote = bot?.remote !== void 0;
-+ const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST);
-+ const drawn = (0, import_react10.useRef)(false);
-+ const carry = drawn.current;
-+ (0, import_react10.useEffect)(() => {
-+ drawn.current = Boolean(host && phase !== null && shown);
-+ }, [host, phase, shown]);
-+ if (!host || phase === null || !shown) return null;
-+ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
-+ return (0, import_react_dom2.createPortal)((0, import_react10.createElement)(
- "div",
-- { className: "bt-event" },
-- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label)
-- );
-+ { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true },
-+ bot ? (0, import_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-+ (0, import_react10.createElement)(
-+ "span",
-+ { className: "bt-typing-text" },
-+ (0, import_react10.createElement)(
-+ "span",
-+ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
-+ (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`),
-+ elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
-+ )
-+ )
-+ ), host);
- }
--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);
-+
-+// 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);
- }
--var scrollParent = (node) => {
-- for (let at = node.parentElement; at; at = at.parentElement) {
-- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at;
-- }
-- return null;
-+
-+// src/client/overlay-hooks.js
-+var import_react11 = require("react");
-+var escapeLayers = [];
-+var onEscapeKey = (event) => {
-+ if (event.key === "Escape") escapeLayers.at(-1)?.current();
- };
--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 }));
-- });
++ 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 observer = new MutationObserver(() => {
++ if (!frame) frame = requestAnimationFrame(scan);
+ });
- };
- (0, import_react6.useEffect)(() => {
- alive.current = true;
- load(void 0);
-+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);
++ scan();
return () => {
- alive.current = false;
-+ escapeLayers.splice(escapeLayers.indexOf(ref), 1);
-+ if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey);
++ cancelAnimationFrame(frame);
++ observer.disconnect();
};
- }, [sessionId]);
- (0, import_react6.useLayoutEffect)(() => {
@@ -8864,7 +7110,8 @@ diff --git a/client.js b/client.js
+ }, []);
}
-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
-- const roster = useRoster((value) => value);
++function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, 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;
@@ -8926,14 +7173,166 @@ diff --git a/client.js b/client.js
- onOpen: (bot) => actions.openExchange(selfId, bot.id),
- whole: true
- });
-- }
++ const running = useSessionStatus((map) => map.get(sessionId)?.running === true);
++ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {}));
++ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
++ const bot = roster.byId[sessionId];
++ const room2 = roster.roomsById[sessionId];
++ const entries2 = (0, import_react10.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
++ const streaming = entries2.some((entry) => entry.text);
++ const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at);
++ const relaying = tools.some((entry) => entry.relay);
++ const awaiting = useAwaitingReply(roster, sessionId);
++ const sendRun = (0, import_react10.useRef)(false);
++ if (!running) sendRun.current = false;
++ else if (pendingInput > 0) sendRun.current = true;
++ const replied = useReplyAfterSend(bot?.remote !== void 0, running && sendRun.current);
++ const visible = bot?.remote !== void 0 ? !streaming && (running ? sendRun.current : awaiting && !replied) : running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
++ const lastRun = (0, import_react10.useRef)({ running, send: false });
++ const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running;
++ lastRun.current = { running, send: sendRun.current };
++ const steady = bridging;
++ const tool = tools.at(-1);
++ const inputRef = (0, import_react10.useRef)(0);
++ const inputAt = Math.max(inputRef.current, pendingInput);
++ inputRef.current = inputAt;
++ const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null;
++ const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
++ const [shown, setShown] = (0, import_react10.useState)(null);
++ const wantKey = want?.key ?? null;
++ const markState = (0, import_react10.useRef)("thinking");
++ if (want) markState.current = want.state;
++ (0, import_react10.useEffect)(() => {
++ if (wantKey === null || shown?.key === wantKey) return void 0;
++ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() });
++ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0;
++ 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]);
++ const [phase, setPhase] = (0, import_react10.useState)(null);
++ const endedAt = (0, import_react10.useRef)(0);
++ const shift = (0, import_react10.useRef)(null);
++ const leaving = (0, import_react10.useRef)(false);
++ leaving.current = phase !== null && !visible;
++ (0, import_react10.useEffect)(() => {
++ 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);
++ timer = setTimeout(() => {
++ shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
++ setPhase((current) => current === "out" ? null : current);
++ }, 140);
++ };
++ if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS);
++ else leave();
++ return () => clearTimeout(timer);
++ }, [visible]);
++ (0, import_react10.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_react10.useEffect)(() => {
++ if (!running) actions.clearLive(sessionId);
++ }, [running, sessionId]);
++ useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
++ const [now, setNow] = (0, import_react10.useState)(Date.now);
++ (0, import_react10.useEffect)(() => {
++ if (phase !== "in") return void 0;
++ const timer = setInterval(() => setNow(Date.now()), 1e4);
++ return () => clearInterval(timer);
++ }, [phase]);
++ const remote = bot?.remote !== void 0;
++ const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST);
++ const drawn = (0, import_react10.useRef)(false);
++ const carry = drawn.current;
++ (0, import_react10.useEffect)(() => {
++ drawn.current = Boolean(host && phase !== null && shown);
++ }, [host, phase, shown]);
++ if (!host || phase === null || !shown) return null;
++ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
++ return (0, import_react_dom2.createPortal)((0, import_react10.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_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
++ (0, import_react10.createElement)(
++ "span",
++ { className: "bt-typing-text" },
++ (0, import_react10.createElement)(
++ "span",
++ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
++ (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`),
++ elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
++ )
++ )
++ ), host);
+ }
+-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];
++var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
++function showsDshRow(dsh, app) {
++ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
++ if (dsh.kind === "npm") return true;
++ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
+ }
+-function 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;
++
++// src/client/overlay-hooks.js
++var import_react11 = require("react");
++var escapeLayers = [];
++var onEscapeKey = (event) => {
++ if (event.key === "Escape") escapeLayers.at(-1)?.current();
++};
++function useEscape(onClose) {
++ const ref = (0, import_react11.useRef)(onClose);
++ ref.current = onClose;
++ (0, import_react11.useEffect)(() => {
++ if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey);
++ escapeLayers.push(ref);
++ return () => {
++ escapeLayers.splice(escapeLayers.indexOf(ref), 1);
++ if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey);
++ };
++ }, []);
++}
+function usePaneLeft() {
+ const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280);
+ (0, import_react11.useLayoutEffect)(() => {
@@ -8963,44 +7362,14 @@ diff --git a/client.js b/client.js
+ const doing = workIn(room2, work, id);
+ const approvals = askedIn(room2, work, id, doing);
+ return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" };
-+ });
- }
--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 busyMembers(room2, work, roster) {
-+ const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]);
-+ return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] }));
- }
--function 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 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 Nothing() {
- return null;
-+function botGroupWork(botId, work, roster) {
-+ return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => {
-+ const entry = entries2.find((item) => item.botId === botId);
-+ const room2 = roster.roomsById[roomId];
-+ return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : [];
-+ });
++function 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 SEPARATOR_GAP_MS = 30 * 60 * 1e3;
-function clockLabel(time, now = Date.now()) {
@@ -9012,14 +7381,10 @@ diff --git a/client.js b/client.js
- if (days === 1) return t("Yesterday {time}", { time: clock4 });
- const sameYear = date.getFullYear() === new Date(now).getFullYear();
- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`;
-+function 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 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 nextTurnStart(turns, turn) {
- let next;
@@ -9028,12 +7393,12 @@ diff --git a/client.js b/client.js
- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate;
- }
- return next === void 0 ? void 0 : turns[next].first;
-+function Face({ entry, size }) {
-+ return (0, import_react12.createElement)(
-+ "span",
-+ { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 },
-+ (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
-+ );
++function botGroupWork(botId, work, roster) {
++ return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => {
++ const entry = entries2.find((item) => item.botId === botId);
++ const room2 = roster.roomsById[roomId];
++ return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : [];
++ });
}
-function openingTurn(turns, firstTurns, hiddenTurns) {
- if (!turns || !firstTurns) return null;
@@ -9043,17 +7408,14 @@ diff --git a/client.js b/client.js
- const listed = firstTurns.split(",").map(Number);
- const earlier = listed.filter((entry) => entry < turn);
- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null;
-+var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : "";
-+function WorkRow({ bot, tool, step, label, at, now, onOpen }) {
-+ const text = `${label} · ${workText(tool, step)}`;
-+ const elapsed = elapsedSince(now, at);
-+ return (0, import_react12.createElement)(
-+ "button",
-+ { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
-+ (0, import_react12.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }),
-+ (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text)),
-+ elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
-+ );
++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 unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
@@ -9078,6 +7440,83 @@ diff --git a/client.js b/client.js
- null,
- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
- fresh ? (0, import_react6.createElement)(NewDivider) : null
++function Face({ entry, size }) {
++ return (0, import_react12.createElement)(
++ "span",
++ { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 },
++ (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
+ );
+ }
+-function Shimmer({ children }) {
+- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
++var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : "";
++function WorkRow({ bot, tool, step, label, at, now, onOpen }) {
++ const text = `${label} · ${workText(tool, step)}`;
++ const elapsed = elapsedSince(now, at);
++ return (0, import_react12.createElement)(
++ "button",
++ { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
++ (0, import_react12.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }),
++ (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text)),
++ elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
++ );
+ }
+-var PHRASES = {
+- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"],
+- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"]
+-};
+-var TOOL_PHRASES = [
+- [/web_?search|search_?web/i, "Searching online"],
+- [/fetch|browse|browser|web/i, "Reading a page"],
+- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"],
+- [/^(read|view|cat)\b|read_/i, "Opening a file"],
+- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"],
+- [/write|edit|patch|apply|replace/i, "Writing changes"],
+- [/image|photo|draw/i, "Working on an image"]
+-];
+-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" };
+-function toolActivity(name, arg, findBot, self) {
+- switch (name) {
+- case "ask_user":
+- return null;
+- case "request_secret":
+- return null;
+- case "list_secrets":
+- return { label: t("Checking the keys"), state: "searching" };
+- case "ask_user_question":
+- return { label: t("Waiting for your answer"), state: "alert" };
+- case "group_relay":
+- return { relay: true, state: "orbit" };
+- case "message_bot": {
+- const target = findBot(arg("to"));
+- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" };
+- }
+- case "stop_bot": {
+- const target = findBot(arg("bot"));
+- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" };
+- }
+- case "create_bot":
+- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" };
+- case "update_bot": {
+- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
+- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" };
+- }
+- case "list_bots":
+- return { label: t("Checking the team"), state: "searching" };
+- case "read_group_chat":
+- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" };
+- case "read_own_chat":
+- return { label: t("Looking back through the chat"), state: "searching" };
+- case "remember":
+- return { label: t("Saving to memory"), state: "working" };
+- case "forget":
+- return { label: t("Updating memory"), state: "working" };
+- case "recall":
+- return { label: t("Checking memory"), state: "searching" };
+- default:
+- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" };
+- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1];
+- return { label: phrase ? t(phrase) : null, state: toolState(name) };
+function SummaryLine({ busy, now, onOpen }) {
+ const { waiting, doing } = summaryText(busy);
+ const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : "";
@@ -9093,10 +7532,8 @@ diff --git a/client.js b/client.js
+ doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null
+ ),
+ elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
- );
- }
--function Shimmer({ children }) {
-- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
++ );
++}
+function MemberDock({ sessionId, useRoster, useWork, actions }) {
+ const roster = useRoster((value) => value);
+ const work = useWork((value) => value);
@@ -9119,7 +7556,7 @@ diff --git a/client.js b/client.js
+ rows.map((entry) => (0, import_react12.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })),
+ rest.length > 0 ? (0, import_react12.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null
+ );
-+ }
+ }
+ return (0, import_react12.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react12.createElement)(WorkRow, {
+ key: item.room.id,
+ bot,
@@ -9131,18 +7568,9 @@ diff --git a/client.js b/client.js
+ onOpen: () => actions.openSession(item.room.id)
+ })));
}
--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"]
+-function workPhrase(tool) {
+- if (!tool) return t("Thinking about it");
+- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
+function useFlowColumn(ref, active, key) {
+ (0, import_react12.useLayoutEffect)(() => {
+ const node = ref.current;
@@ -9159,7 +7587,14 @@ diff --git a/client.js b/client.js
+ for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target);
+ return () => observer.disconnect();
+ }, [active, key]);
-+}
+ }
+-function 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 useNow(active) {
+ const [now, setNow] = (0, import_react12.useState)(Date.now);
+ (0, import_react12.useEffect)(() => {
@@ -9168,56 +7603,17 @@ diff --git a/client.js b/client.js
+ return () => clearInterval(timer);
+ }, [active]);
+ return now;
-+}
+ }
+-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;
+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"],
+ ...approval.roomId ? [["always-room", "Always allow for this group"]] : []
- ];
--var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" };
--function toolActivity(name, arg, findBot, self) {
-- switch (name) {
-- case "ask_user":
-- return null;
-- case "request_secret":
-- return null;
-- case "list_secrets":
-- return { label: t("Checking the keys"), state: "searching" };
-- case "ask_user_question":
-- return { label: t("Waiting for your answer"), state: "alert" };
-- case "group_relay":
-- return { relay: true, state: "orbit" };
-- case "message_bot": {
-- const target = findBot(arg("to"));
-- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" };
-- }
-- case "stop_bot": {
-- const target = findBot(arg("bot"));
-- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" };
-- }
-- case "create_bot":
-- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" };
-- case "update_bot": {
-- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
-- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" };
-- }
-- case "list_bots":
-- return { label: t("Checking the team"), state: "searching" };
-- case "read_group_chat":
-- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" };
-- case "read_own_chat":
-- return { label: t("Looking back through the chat"), state: "searching" };
-- case "remember":
-- return { label: t("Saving to memory"), state: "working" };
-- case "forget":
-- return { label: t("Updating memory"), state: "working" };
-- case "recall":
-- return { label: t("Checking memory"), state: "searching" };
-- default:
-- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" };
-- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1];
-- return { label: phrase ? t(phrase) : null, state: toolState(name) };
-- }
++];
+function ApprovalCard({ approval, bot, actions }) {
+ const [busy, setBusy] = (0, import_react12.useState)(false);
+ const answer = (choice) => {
@@ -9243,10 +7639,7 @@ diff --git a/client.js b/client.js
+ (0, import_react12.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name }))
+ )
+ );
- }
--function workPhrase(tool) {
-- if (!tool) return t("Thinking about it");
-- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
++}
+function MemberWork({ entry, bot, room: room2, actions, now }) {
+ const [stopping, setStopping] = (0, import_react12.useState)(false);
+ if (entry.state === "ask") {
@@ -9272,25 +7665,13 @@ diff --git a/client.js b/client.js
+ entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null
+ )
+ );
- }
--function 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 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 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;
++}
+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}
@@ -9930,10 +8311,16 @@ diff --git a/client.js b/client.js
- 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;
+function whenText(card, now) {
+ if (card.status === "running" || card.status === "task") {
+ return typeof card.since === "number" && now - card.since >= 6e4 ? t("for {time}", { time: elapsedLabel(now - card.since) }) : t("just started");
-+ }
+ }
+ return typeof card.at === "number" ? relativeShort(card.at) : "";
+}
+function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) {
@@ -10029,13 +8416,7 @@ diff --git a/client.js b/client.js
+ if (pending === void 0) {
+ setBusy("");
+ return;
- }
-- case "remember":
-- case "forget":
-- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null;
-- default:
-- return null;
-- }
++ }
+ void Promise.resolve(pending).then(() => {
+ setAnswered((value) => ({ ...value, [card.id]: true }));
+ const name = card.bot?.name ?? "";
@@ -10382,18 +8763,6 @@ diff --git a/client.js b/client.js
- 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;
+ };
+ 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 }));
@@ -10430,43 +8799,7 @@ diff --git a/client.js b/client.js
+ default:
+ return t("Not started");
+ }
- }
--function useRowEntrances(replacing) {
-- const check = (0, import_react7.useRef)(replacing);
-- check.current = replacing;
-- (0, import_react7.useEffect)(() => {
-- const seen = /* @__PURE__ */ new WeakSet();
-- const started = Date.now();
-- let column = null;
-- let frame = 0;
-- const settle = (event) => {
-- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return;
-- delete event.currentTarget.dataset.btEnter;
-- event.currentTarget.removeEventListener("animationend", settle);
-- };
-- const scan = () => {
-- frame = 0;
-- const next = document.querySelector("[data-chat-flow]");
-- if (next !== column) {
-- observer.disconnect();
-- column = next;
-- if (column) observer.observe(column, { childList: true, subtree: true });
-- }
-- if (!column) return;
-- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")];
-- let last = -1;
-- items.forEach((item, index) => {
-- if (seen.has(item)) last = index;
-- });
-- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0);
-- for (const item of fresh) seen.add(item);
-- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return;
-- for (const item of fresh) {
-- if (items.indexOf(item) < last) continue;
-- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind);
-- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new";
-- item.addEventListener("animationend", settle);
-- }
++}
+function dropLabel(column) {
+ if (column === "in-progress") return t("Start task");
+ if (column === "done") return t("Complete task");
@@ -10517,9 +8850,10 @@ diff --git a/client.js b/client.js
+ }).catch(() => void 0);
};
- const observer = new MutationObserver(() => {
-- if (!frame) frame = requestAnimationFrame(scan);
+- if (!frame) frame = requestAnimationFrame(find);
- });
-- scan();
+- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
+- find();
+ load();
+ const timer = setInterval(load, POLL_MS3);
return () => {
@@ -10528,7 +8862,8 @@ diff --git a/client.js b/client.js
+ stopped = true;
+ clearInterval(timer);
};
-- }, []);
+- }, [active]);
+- return host;
+ }, [key]);
+ const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId);
+ const replace = (task) => {
@@ -10540,84 +8875,41 @@ diff --git a/client.js b/client.js
+ const done = scope?.kind === "group" ? archive : archive.filter((task) => !scope?.botId || task.ownerBotId === scope.botId);
+ return { own, tasks, archive: done, replace };
}
--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;
-- }
-- const timer = setTimeout(make, wait);
-- return () => clearTimeout(timer);
-- }, [wantKey, shown]);
-- const [phase, setPhase] = (0, import_react7.useState)(null);
-- const endedAt = (0, import_react7.useRef)(0);
-- const shift = (0, import_react7.useRef)(null);
-- const leaving = (0, import_react7.useRef)(false);
-- leaving.current = phase !== null && !visible;
-- (0, import_react7.useEffect)(() => {
-- if (visible) {
-- setPhase("in");
-- return void 0;
-- }
-- setPhase((current) => current === "in" ? "out" : current);
-- const timer = setTimeout(() => {
-- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
-- setPhase((current) => current === "out" ? null : current);
-- }, 140);
-- return () => clearTimeout(timer);
-- }, [visible]);
-- (0, import_react7.useLayoutEffect)(() => {
-- if (phase !== null) return;
-- endedAt.current = Date.now();
-- setShown(null);
-- const before = shift.current;
-- shift.current = null;
-- const scroller = document.querySelector("[data-conversation-scroll]");
-- if (before === null || !scroller || reducedMotion()) return;
-- const delta = before - scroller.scrollTop;
-- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" });
-- }, [phase]);
-- (0, import_react7.useEffect)(() => {
-- if (!running) actions.clearLive(sessionId);
-- }, [running, sessionId]);
-- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
-- const [now, setNow] = (0, import_react7.useState)(Date.now);
+-function useRowEntrances(replacing) {
+- const check = (0, import_react7.useRef)(replacing);
+- check.current = replacing;
- (0, import_react7.useEffect)(() => {
-- if (phase !== "in") return void 0;
-- const timer = setInterval(() => setNow(Date.now()), 1e4);
-- return () => clearInterval(timer);
-- }, [phase]);
-- const host = useRunningHost(running);
-- if (!host || phase === null || !shown) return null;
-- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
-- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)(
-- "div",
-- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true },
-- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-- (0, import_react7.createElement)(
+- const seen = /* @__PURE__ */ new WeakSet();
+- const started = Date.now();
+- let column = null;
+- let frame = 0;
+- const settle = (event) => {
+- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return;
+- delete event.currentTarget.dataset.btEnter;
+- event.currentTarget.removeEventListener("animationend", settle);
+- };
+- const scan = () => {
+- frame = 0;
+- const next = document.querySelector("[data-chat-flow]");
+- if (next !== column) {
+- observer.disconnect();
+- column = next;
+- if (column) observer.observe(column, { childList: true, subtree: true });
+- }
+- if (!column) return;
+- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")];
+- let last = -1;
+- items.forEach((item, index) => {
+- if (seen.has(item)) last = index;
+- });
+- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0);
+- for (const item of fresh) seen.add(item);
+- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return;
+- for (const item of fresh) {
+- if (items.indexOf(item) < last) continue;
+- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind);
+- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new";
+- item.addEventListener("animationend", settle);
+function RailButton({ row, selected, meta, onSelect }) {
+ return (0, import_react15.createElement)(
+ "button",
@@ -10664,16 +8956,11 @@ diff --git a/client.js b/client.js
+ question ? (0, import_react15.createElement)("span", { key: "wait", className: "bt-board-wait" }, question) : null
+ ],
+ footer: (0, import_react15.createElement)(
- "span",
-- { className: "bt-typing-text" },
-- (0, import_react7.createElement)(
++ "span",
+ { className: "bt-board-foot" },
+ who ? (0, import_react15.createElement)("span", { className: "bt-board-who" }, who) : null,
+ (0, import_react15.createElement)(
- "span",
-- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
-- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`),
-- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
++ "span",
+ { className: "bt-board-progress" },
+ (0, import_react15.createElement)(
+ "span",
@@ -10715,7 +9002,17 @@ diff --git a/client.js b/client.js
+ const id = event.dataTransfer?.getData("text/plain");
+ if (id) onMove?.(id, column);
+ onDrag?.(null);
-+ }
+ }
+- };
+- const observer = new MutationObserver(() => {
+- if (!frame) frame = requestAnimationFrame(scan);
+- });
+- scan();
+- return () => {
+- cancelAnimationFrame(frame);
+- observer.disconnect();
+- };
+- }, []);
+ },
+ (0, import_react15.createElement)(
+ "h3",
@@ -10743,7 +9040,81 @@ diff --git a/client.js b/client.js
+ lane.tasks.map((task) => (0, import_react15.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
+ )) : plain.map((task) => (0, import_react15.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
+ );
-+}
+ }
+-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;
+- }
+- const timer = setTimeout(make, wait);
+- return () => clearTimeout(timer);
+- }, [wantKey, shown]);
+- const [phase, setPhase] = (0, import_react7.useState)(null);
+- const endedAt = (0, import_react7.useRef)(0);
+- const shift = (0, import_react7.useRef)(null);
+- const leaving = (0, import_react7.useRef)(false);
+- leaving.current = phase !== null && !visible;
+- (0, import_react7.useEffect)(() => {
+- if (visible) {
+- setPhase("in");
+- return void 0;
+- }
+- setPhase((current) => current === "in" ? "out" : current);
+- const timer = setTimeout(() => {
+- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
+- setPhase((current) => current === "out" ? null : current);
+- }, 140);
+- return () => clearTimeout(timer);
+- }, [visible]);
+- (0, import_react7.useLayoutEffect)(() => {
+- if (phase !== null) return;
+- endedAt.current = Date.now();
+- setShown(null);
+- const before = shift.current;
+- shift.current = null;
+- const scroller = document.querySelector("[data-conversation-scroll]");
+- if (before === null || !scroller || reducedMotion()) return;
+- const delta = before - scroller.scrollTop;
+- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" });
+- }, [phase]);
+- (0, import_react7.useEffect)(() => {
+- if (!running) actions.clearLive(sessionId);
+- }, [running, sessionId]);
+- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
+- const [now, setNow] = (0, import_react7.useState)(Date.now);
+- (0, import_react7.useEffect)(() => {
+- if (phase !== "in") return void 0;
+- const timer = setInterval(() => setNow(Date.now()), 1e4);
+- return () => clearInterval(timer);
+- }, [phase]);
+- const host = useRunningHost(running);
+- if (!host || phase === null || !shown) return null;
+- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
+- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)(
+function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) {
+ const columns = boardColumns(tasks);
+ const lanes = scope?.kind === "group";
@@ -10796,20 +9167,10 @@ diff --git a/client.js b/client.js
+ onMove,
+ onDrag
+ }))
- )
- )
-- ), 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);
++}
+function KanbanRow({ wide, useUi, actions }) {
+ const open = useUi ? useUi((value) => Boolean(value.board)) : false;
+ const label = t("Tasks");
@@ -10874,7 +9235,18 @@ diff --git a/client.js b/client.js
+ };
+ const name = scopeName(roster, scope);
+ 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)(
+- "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
+- )
+ { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 },
+ (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }),
+ (0, import_react15.createElement)(
@@ -10903,9 +9275,19 @@ diff --git a/client.js b/client.js
+ onDrag: setDragging,
+ onCreate: create
+ })
-+ )
+ )
+- ), 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);
+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)}
@@ -10967,7 +9349,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}
-+}
+ }
+`;
// src/client/sidebar.js
diff --git a/patches/ds-bot/files/src/client/activity-row-host.js b/patches/ds-bot/files/src/client/activity-row-host.js
new file mode 100644
index 000000000..ad6f25e6e
--- /dev/null
+++ b/patches/ds-bot/files/src/client/activity-row-host.js
@@ -0,0 +1,17 @@
+// Pure host helpers for the activity row (#1626). Kept free of React so unit tests
+// import them without a DOM renderer.
+
+// DSH 0.2.0 put RunningStatus as a direct child of [data-chat-flow]. DSH 0.2.1+
+// appends it inside the conversation.chat.flow slot, so a direct-child selector
+// misses the host and the Bot avatar + shimmer never portals (#1626).
+export const RUNNING_HOST = '[data-chat-flow] [data-chat-running]'
+export const TAIL_HOST = 'tail'
+
+/** Resolve the shell running-status node under a document or flow root (0.2.0 or 0.2.1+). */
+export function findRunningHostIn(root) {
+ if (!root || typeof root.querySelector !== 'function') return null
+ if (typeof root.matches === 'function' && root.matches('[data-chat-running]')) return root
+ // An Element (the flow column) scopes to descendants; a Document uses the full path.
+ if (typeof Element !== 'undefined' && root instanceof Element) return root.querySelector('[data-chat-running]')
+ return root.querySelector(RUNNING_HOST)
+}
diff --git a/patches/ds-bot/files/test/activity-row-host.test.js b/patches/ds-bot/files/test/activity-row-host.test.js
new file mode 100644
index 000000000..6bda40565
--- /dev/null
+++ b/patches/ds-bot/files/test/activity-row-host.test.js
@@ -0,0 +1,46 @@
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { join } from 'node:path'
+import { readFileSync } from 'node:fs'
+import { test } from 'node:test'
+import { findRunningHostIn, RUNNING_HOST } from '../src/client/activity-row-host.js'
+
+// jsdom comes from the DSH harness (test/harness-resolve.mjs sets DSH_HARNESS).
+const { JSDOM } = createRequire(join(process.env.DSH_HARNESS, 'package.json'))('jsdom')
+
+test('RUNNING_HOST is a descendant selector so DSH 0.2.1 nested RunningStatus still matches (#1626)', () => {
+ assert.equal(RUNNING_HOST, '[data-chat-flow] [data-chat-running]')
+ assert.equal(RUNNING_HOST.includes('>'), false, 'must not require a direct child of the flow column')
+ const src = readFileSync(new URL('../src/client/activity-row.js', import.meta.url), 'utf8')
+ assert.match(src, /activity-row-host\.js/)
+ assert.doesNotMatch(src, /\[data-chat-flow\] > \[data-chat-running\]/)
+ const hostSrc = readFileSync(new URL('../src/client/activity-row-host.js', import.meta.url), 'utf8')
+ assert.match(hostSrc, /conversation\.chat\.flow/)
+})
+
+test('findRunningHostIn finds RunningStatus as a direct child (DSH 0.2.0 layout)', () => {
+ const { document } = new JSDOM(``).window
+ const host = findRunningHostIn(document)
+ assert.equal(host?.id, 'run')
+ assert.equal(findRunningHostIn(document.querySelector('[data-chat-flow]'))?.id, 'run')
+})
+
+test('findRunningHostIn finds RunningStatus nested under conversation.chat.flow (DSH 0.2.1+)', () => {
+ const { document } = new JSDOM(``).window
+ assert.equal(document.querySelector('[data-chat-flow] > [data-chat-running]'), null, 'direct-child selector misses 0.2.1')
+ assert.equal(findRunningHostIn(document)?.id, 'nested-run')
+ assert.equal(findRunningHostIn(document.querySelector('[data-chat-flow]'))?.id, 'nested-run')
+})
+
+test('findRunningHostIn returns null when the running host is absent', () => {
+ const { document } = new JSDOM(``).window
+ assert.equal(findRunningHostIn(document), null)
+ assert.equal(findRunningHostIn(null), null)
+})