From d238d54331cb830e8f6902911d1b32610787c096 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 31 Oct 2026 14:49:15 +0800 Subject: [PATCH 1/6] #1900: read the team dossier once per board open, not on every poll readProjectBoard takes the previous dossier answer back as `cached.team` and skips the `project-team` call when it has one, so the five-second refresh (#1814/#1824) draws the answer the open read took instead of re-reading the whole team dossier (host `projectTeam` calls `memoryDigest` per row: ~16 KB every 5 s, ~192 KB/minute per open board). A dossier read that throws is now non-fatal: rows, done, tasks and queues still draw and only `team` gives up as `{ rows: [] }`. The empty-project answer stays reserved for a Project that really is absent. Red first in core/test/project-board-panels.test.js: with the base source the two new cases fail (17 pass / 2 fail), with this tip they pass (19 / 0). --- .../1900-team-dossier-off-poll.changed.md | 1 + core/client.js | 13 +- core/src/client/project-board.js | 29 +- core/test/project-board-panels.test.js | 47 + patches/ds-bot/0008-client-bundle.patch | 8993 +++++------------ .../ds-bot/files/src/client/project-board.js | 29 +- .../files/test/project-board-panels.test.js | 47 + 7 files changed, 2730 insertions(+), 6429 deletions(-) create mode 100644 changelog.d/1900-team-dossier-off-poll.changed.md diff --git a/changelog.d/1900-team-dossier-off-poll.changed.md b/changelog.d/1900-team-dossier-off-poll.changed.md new file mode 100644 index 000000000..8b4072983 --- /dev/null +++ b/changelog.d/1900-team-dossier-off-poll.changed.md @@ -0,0 +1 @@ +The Project board reads the team dossier (personas, memory heads, skills) once when the board opens instead of on every five-second refresh, so an open board no longer pulls about 16 KB of unchanged data per tick ([#1900](https://github.com/ThinkFlowLab/FlyBot/issues/1900)). A dossier read that fails now leaves the board drawn with an empty team area instead of falling back to the empty-project state. diff --git a/core/client.js b/core/client.js index aeef9850b..d4ef5f7bc 100644 --- a/core/client.js +++ b/core/client.js @@ -11260,17 +11260,19 @@ function ProjectMembersPanel({ project, rows, onClose } = {}) { ) ); } -async function readProjectBoard(read, wanted) { +async function readProjectBoard(read, wanted, cached) { const listing = await read("projects"); const projects = Array.isArray(listing?.projects) ? listing.projects : []; const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); if (project === void 0) return { project: null, rows: [], done: [], canManage: false, tasks: [], team: { rows: [] } }; const zone = typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function" ? void 0 : Intl.DateTimeFormat().resolvedOptions().timeZone; + const seeded = cached !== null && typeof cached === "object" && cached.team !== void 0; + const teamAnswer = seeded ? Promise.resolve(cached.team) : Promise.resolve().then(() => read("project-team", { id: project.id })).catch(() => null); const [record, board, taskList, team] = await Promise.all([ read("project-read", { id: project.id }), read("project-board", { id: project.id, timeZone: zone }), read("tasks", {}), - read("project-team", { id: project.id }) + teamAnswer ]); const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null }; @@ -11328,7 +11330,12 @@ function ProjectBoardOverlay({ actions, call: request } = {}) { } const wanted = hash.slice("#project".length).replace(/^=/, ""); let gone = false; - const loadBoard = () => readProjectBoard(read, wanted); + let teamAnswer; + const loadBoard = async () => { + const next = await readProjectBoard(read, wanted, teamAnswer === void 0 ? void 0 : { team: teamAnswer }); + if (next.project !== null) teamAnswer = next.team; + return next; + }; const refresh = async (first) => { try { const next = await loadBoard(); diff --git a/core/src/client/project-board.js b/core/src/client/project-board.js index 4f8e0cbbc..a3ba1a31e 100644 --- a/core/src/client/project-board.js +++ b/core/src/client/project-board.js @@ -866,7 +866,11 @@ export function ProjectMembersPanel({ project, rows, onClose } = {}) { // #1864: one read for the whole board, exported so a test can prove the dossier comes from a single // `project-team` call. `read` is the channel seam (`call` in the app). -export async function readProjectBoard(read, wanted) { +// #1900: the dossier is the one answer in that read which does not change on a five-second clock, so +// the caller hands back the answer it already has (`cached.team`) and the read skips the call: the +// board reads it once when it opens and every poll after that draws that same answer. The other +// three channels are read every time — they are the reason a poll exists. +export async function readProjectBoard(read, wanted, cached) { const listing = await read('projects') const projects = Array.isArray(listing?.projects) ? listing.projects : [] const project = wanted === '' ? projects[0] : projects.find(item => item.id === wanted) @@ -874,11 +878,20 @@ export async function readProjectBoard(read, wanted) { const zone = typeof Intl === 'undefined' || typeof Intl.DateTimeFormat !== 'function' ? undefined : Intl.DateTimeFormat().resolvedOptions().timeZone + const seeded = cached !== null && typeof cached === 'object' && cached.team !== undefined + // #1900: a dossier that throws is the team area's loss, not the board's. The rejection used to + // reject the whole `Promise.all`, and the first paint turned that into the empty state — a broken + // persona file made a Project that exists read as "No Project yet", which is the one answer + // `:873` reserves for a Project that really is gone. Rows, `done`, tasks and queues still draw; + // only `team` gives up, as `{ rows: [] }` below. + const teamAnswer = seeded + ? Promise.resolve(cached.team) + : Promise.resolve().then(() => read('project-team', { id: project.id })).catch(() => null) const [record, board, taskList, team] = await Promise.all([ read('project-read', { id: project.id }), read('project-board', { id: project.id, timeZone: zone }), read('tasks', {}), - read('project-team', { id: project.id }), + teamAnswer, ]) const tasks = Array.isArray(taskList) ? taskList : (taskList?.tasks ?? []) // The groups are the same read as the rows, so the header cannot disagree with the cards. @@ -951,7 +964,17 @@ export function ProjectBoardOverlay({ actions, call: request } = {}) { let gone = false // #1864: the board, its record, its rows, the task texts and the team dossier are one read path, // and the dossier comes from the single `project-team` call that read makes. - const loadBoard = () => readProjectBoard(read, wanted) + // #1900: that one dossier answer is held for as long as this board is open. The effect re-runs + // per `#project` entry, so re-opening the board reads the dossier again and a persona or memory + // edit lands then; the five-second poll only draws the answer the open read already took. + let teamAnswer + const loadBoard = async () => { + const next = await readProjectBoard(read, wanted, teamAnswer === undefined ? undefined : { team: teamAnswer }) + // A board with no Project took no dossier read at all, so it seeds nothing: the first poll + // that does find the Project reads the dossier itself instead of caching the empty answer. + if (next.project !== null) teamAnswer = next.team + return next + } const refresh = async (first) => { try { const next = await loadBoard() diff --git a/core/test/project-board-panels.test.js b/core/test/project-board-panels.test.js index 6f4a2de11..cbe1fcdbb 100644 --- a/core/test/project-board-panels.test.js +++ b/core/test/project-board-panels.test.js @@ -537,6 +537,53 @@ test('the board reads the dossier from exactly one project-team call', async () assert.deepEqual(opened.map(entry => entry.botId), ['bot-chief']) }) +// #1900: the dossier only changes when someone edits a persona or a memory file, so it is read +// once, when the board opens, and the five-second poll (#1814/#1824) draws that answer instead of +// asking again. The poll was re-reading the whole team dossier — host side a `memoryDigest` per row, +// ~16 KB every 5 s — to receive an answer that had not changed. +test('#1900: the five-second poll reuses the dossier answer the open read took', async () => { + resetSlots() + const calls = [] + const read = async (endpoint) => { + calls.push(endpoint) + if (endpoint === 'projects') return { projects: [{ id: 'project-1', name: 'FlyBot', role: 'owner', choices: [] }] } + if (endpoint === 'project-read') return { project: projectRecord(), canManage: true } + if (endpoint === 'project-board') return boardAnswer() + if (endpoint === 'tasks') return [] + if (endpoint === 'project-team') return { rows: [dossier(), null, { ...dossier(), botId: 'bot-dev', name: 'Dev' }] } + throw new Error(`unexpected endpoint ${endpoint}`) + } + const count = () => calls.filter(endpoint => endpoint === 'project-team').length + const opened = await readProjectBoard(read, 'project-1') + assert.equal(count(), 1, 'opening the board reads the dossier once') + const polled = await readProjectBoard(read, 'project-1', { team: opened.team }) + assert.equal(count(), 1, 'the poll does not ask for the dossier again') + assert.equal(polled.team.rows.length, 3, 'the poll keeps drawing the one answer') + assert.deepEqual(polled.team.rows.map(row => row?.botId), opened.team.rows.map(row => row?.botId)) +}) + +// #1900: a dossier that fails is not the board failing. It keeps its rows, its `done` group and its +// tasks, and the team area draws empty — the empty-project answer (`project-board.js:873`) stays +// reserved for a Project that really is absent, so a broken dossier cannot paint "No Project yet". +test('#1900: a dossier read that throws leaves the board drawn and the team empty', async () => { + resetSlots() + const read = async (endpoint) => { + if (endpoint === 'projects') return { projects: [{ id: 'project-1', name: 'FlyBot', role: 'owner', choices: [] }] } + if (endpoint === 'project-read') return { project: projectRecord(), canManage: true } + if (endpoint === 'project-board') return { ...boardAnswer(), tasks: [{ id: 'row-1' }], done: [{ id: 'done-1' }] } + if (endpoint === 'tasks') return [{ id: 'task-1' }] + if (endpoint === 'project-team') throw new Error('the dossier is unreadable this tick') + throw new Error(`unexpected endpoint ${endpoint}`) + } + const state = await readProjectBoard(read, 'project-1') + assert.equal(state.project?.id, 'project-1', 'the Project still draws: no empty state') + assert.equal(state.rows.length, 1) + assert.equal(state.done.length, 1) + assert.equal(state.tasks.length, 1) + assert.deepEqual(Object.keys(state.team).sort(), ['rows'], 'the team area is the one thing that gives up') + assert.equal(state.team.rows.length, 0) +}) + // #1892: the role pool is a rank list, not a Bot the board holds a dossier answer for. // `project-team` answers rows keyed by the Project's own Bots, so a pool click asked for a row the // host never sent and drew an empty panel. A pool entry now reads `@id` beside its rank; the Chief diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 0c2147401..d02122982 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1717,7 +1717,16 @@ diff --git a/client.js b/client.js - "--bt-tag-bg": rgba(mode.text, dark ? 0.09 : 0.06), - "--bt-tag-ink": rgba(mode.text, 0.58) - }; --} ++// src/client/cluster-order.js ++function clusterOrder(people2) { ++ const list = Array.isArray(people2) ? people2 : []; ++ const remote = (bot) => Boolean(bot?.remote); ++ if (!list.some(remote)) return list; ++ const locals = list.filter((bot) => !remote(bot)); ++ const remotes = list.filter(remote); ++ if (list.length <= 4) return [...remotes, ...locals]; ++ return [...locals, ...remotes]; + } -var declarations = (vars) => Object.entries(vars).map(([name, value]) => `${name}:${value}`).join(";"); -var themeCss = (theme, accent) => `html body{${declarations(themeVars(theme.light, false, accent ?? theme.light.accent))}} -html body[data-ds-dark-theme]{${declarations(themeVars(theme.dark, true, accent ?? theme.dark.accent))}} @@ -1760,16 +1769,7 @@ diff --git a/client.js b/client.js - else delete SHAPES[id]; - refresh(); - }; -+// src/client/cluster-order.js -+function clusterOrder(people2) { -+ const list = Array.isArray(people2) ? people2 : []; -+ const remote = (bot) => Boolean(bot?.remote); -+ if (!list.some(remote)) return list; -+ const locals = list.filter((bot) => !remote(bot)); -+ const remotes = list.filter(remote); -+ if (list.length <= 4) return [...remotes, ...locals]; -+ return [...locals, ...remotes]; - } +-} -function registerTheme(id, theme) { - if (typeof id !== "string" || !/^[a-z][\w-]{0,31}$/.test(id)) throw new Error("Theme ids are lowercase words"); - for (const mode of ["light", "dark"]) { @@ -1832,11 +1832,6 @@ diff --git a/client.js b/client.js } return spans; } -- --// src/client/text.js --var MENTION_ORIGIN = "https://bots.local/"; --function linkMentions(text, bots, selfId) { -- if (!text || !text.includes("@")) return text; +function unmatchedMentions(text2, bots, { references = [] } = {}) { + const value = String(text2 ?? ""); + const skip = [ @@ -1884,9 +1879,11 @@ diff --git a/client.js b/client.js + } + return stripFence(out).replace(/\s+/g, " ").trim(); +} -+ -+// src/client/text.js -+var MENTION_ORIGIN = "https://bots.local/"; + + // src/client/text.js + var MENTION_ORIGIN = "https://bots.local/"; +-function linkMentions(text, bots, selfId) { +- if (!text || !text.includes("@")) return text; +function linkMentions(text2, bots, selfId) { + if (!text2 || !text2.includes("@")) return text2; + const ordered = [...bots ?? []].sort((a, b) => Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); @@ -2124,65 +2121,16 @@ 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,901 +1624,1981 @@ function usageOf(list, ids) { +@@ -1624,9 +1624,982 @@ function usageOf(list, ids) { const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } -- --// src/client/motion.js --var import_react = require("react"); --var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; --function springEasing(stiffness, damping) { -- const w0 = Math.sqrt(stiffness); -- const zeta = damping / (2 * w0); -- const at = (t2) => { -- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); -- const wd = w0 * Math.sqrt(1 - zeta * zeta); -- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); -- }; -- let end = 0.05; -- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; -- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); -- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; +function faceRowTitle(title) { + const text2 = String(title ?? ""); + const named = text2.match(/^Face-to-face · (.+) · (\d{6})$/); + if (named) return `${named[2]} · ${named[1]}`; + return text2.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text2; - } --var SPRING_SHAPE = springEasing(400, 31.2); --var SPRING_TAP = springEasing(900, 48); --var SPRING_LIST = springEasing(484, 35.2); --var SPRING_STRETCH = springEasing(320, 20); --var SPRING_POP = springEasing(500, 22); --var SPRING_LEAD = springEasing(784, 48.2); --var SPRING_TRAIL = springEasing(289, 29.2); --var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; --var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); --function liveSprings(apply2) { -- const values = /* @__PURE__ */ new Map(); -- let frame = 0; -- let last = 0; -- const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); -- const step = (now) => { -- const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); -- last = now; -- const steps = Math.ceil(dt * 240); -- const h34 = dt / steps; -- let moving = false; -- for (const value of values.values()) { -- for (let index = 0; index < steps; index += 1) { -- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; -- value.x += value.v * h34; -- } -- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { -- value.x = value.to; -- value.v = 0; -- } else moving = true; -- } -- apply2(read()); -- frame = moving ? requestAnimationFrame(step) : 0; -- }; ++} +function modelOf(bot, roster) { + const ref = bot.model ?? bot.appliedModel; + if (!ref) return void 0; @@ -2197,84 +2145,28 @@ diff --git a/client.js b/client.js +function awayGroups(groups, roomsById) { + return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face"); +} -+ + +-// src/client/motion.js +// src/client/sources.js +function createSource(initial) { + let value = initial; + const listeners2 = /* @__PURE__ */ new Set(); - return { -- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, -- set(targets, curves, jump) { -- for (const [key, to] of Object.entries(targets)) { -- const value = values.get(key); -- const { stiffness: k, damping: c } = curves[key]; -- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); -- else Object.assign(value, { to, k, c }); -- } -- if (jump) { -- cancelAnimationFrame(frame); -- frame = 0; -- apply2(read()); -- return; -- } -- if (!frame) { -- last = performance.now(); -- frame = requestAnimationFrame(step); -- } ++ return { + getSnapshot: () => value, + subscribe: (listener) => { + listeners2.add(listener); + return () => { + listeners2.delete(listener); + }; - }, -- stop: () => cancelAnimationFrame(frame) ++ }, + set(next) { + const resolved = typeof next === "function" ? next(value) : next; + if (resolved === value) return; + value = resolved; + for (const listener of [...listeners2]) listener(); + } - }; - } --var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; --function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { -- const ownBox = (0, import_react.useRef)(null); -- const box = given ?? ownBox; -- const thumb = (0, import_react.useRef)(null); -- const own = (0, import_react.useRef)(null); -- if (own.current === null) { -- own.current = { -- key: void 0, -- node: null, -- observer: null, -- springs: liveSprings(({ start, end }) => { -- const knob = thumb.current; -- if (!knob) return; -- knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; -- knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; -- }) -- }; -- } -- const place = (jump) => { -- const container = box.current; -- const knob = thumb.current; -- if (!container || !knob) return; -- const chosen = container.querySelector(selector); -- if (!chosen) { -- knob.dataset.hidden = ""; -- return; -- } -- const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; -- const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); -- const { springs } = own.current; -- const before = springs.target(); -- const hidden = knob.dataset.hidden !== void 0; -- if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; -- delete knob.dataset.hidden; -- const forward = before === null || start >= before.start; -- springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); -- container.dataset.glide = ""; ++ }; ++} +var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], humans: [], self: null, agents: [], exchangeTurns: {}, remoteTypedTurns: {}, remoteAwaiting: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }; +var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id); +function indexRoster(value) { @@ -2287,26 +2179,7 @@ diff --git a/client.js b/client.js + if (prev && prev !== bot && !prev.remote && bot?.remote) return; + if (prev && prev !== bot && prev.id === id) return; + byId[id] = bot; - }; -- (0, import_react.useLayoutEffect)(() => { -- const state = own.current; -- const node = box.current; -- if (node === state.node && key === state.key) return; -- const fresh = node !== state.node; -- state.key = key; -- if (fresh) { -- state.observer?.disconnect(); -- state.node = node; -- state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; -- state.observer?.observe(node); -- } -- place(fresh); -- }); -- (0, import_react.useEffect)(() => () => { -- own.current.observer?.disconnect(); -- own.current.springs.stop(); -- }, []); -- return { box, thumb }; ++ }; + for (const bot of value.bots) { + for (const id of [...bot.parts ?? [], bot.sessionId ?? bot.id, bot.id]) claim(bot, id); + } @@ -2319,75 +2192,20 @@ diff --git a/client.js b/client.js + for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id; + } + return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }; - } --function Segmented({ label, value, className, children }) { -- const glide = useGlide(value); -- return (0, import_react.createElement)( -- "div", -- { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, -- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- children -- ); ++} +function rosterBot(roster, id) { + if (id === void 0 || id === null || id === "") return void 0; + const list = roster?.bots ?? []; + const own = list.find((bot) => bot && !bot.remote && (bot.id === id || bot.sessionId === id || (bot.parts ?? []).includes(id))); + if (own) return own; + return roster?.byId?.[id]; - } --function useFlip(list, key, selector, id = (node) => node.dataset.flip) { -- const tops = (0, import_react.useRef)(null); -- (0, import_react.useLayoutEffect)(() => { -- const container = list.current; -- if (!container) return; -- const next = /* @__PURE__ */ new Map(); -- const moving = tops.current !== null && !reducedMotion(); -- for (const node of container.querySelectorAll(selector)) { -- const name = id(node); -- const top = node.offsetTop; -- next.set(name, top); -- if (!moving) continue; -- const before = tops.current.get(name); -- if (before === void 0) { -- node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); -- } else if (Math.abs(before - top) > 0.5) { -- node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); -- } -- } -- tops.current = next; -- }, [key]); ++} +function botByName(bots, name) { + const wanted = String(name ?? "").toLowerCase(); + if (wanted === "") return void 0; + const list = Array.isArray(bots) ? bots : []; + return list.find((bot) => bot && !bot.remote && String(bot.name ?? "").toLowerCase() === wanted) ?? list.find((bot) => bot && String(bot.name ?? "").toLowerCase() === wanted); - } --function useMorph(box, key) { -- const last = (0, import_react.useRef)({ key, height: null }); -- (0, import_react.useLayoutEffect)(() => { -- const node = box.current; -- const state = last.current; -- if (!node) { -- last.current = { key, height: null }; -- return; -- } -- const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); -- if (key === state.key) { -- if (running.length === 0) state.height = node.offsetHeight; -- return; -- } -- const from = running.length ? node.offsetHeight : state.height; -- for (const animation2 of running) animation2.cancel(); -- const to = node.offsetHeight; -- last.current = { key, height: to }; -- if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; -- node.dataset.morph = ""; -- const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); -- animation.onfinish = () => { -- delete node.dataset.morph; -- }; -- animation.oncancel = () => { -- 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; @@ -2404,7 +2222,7 @@ diff --git a/client.js b/client.js +} + +// src/client/mark.js -+var import_react = require("react"); + var import_react = require("react"); +var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; @@ -2421,8 +2239,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); @@ -2440,9 +2257,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)); @@ -2522,22 +2337,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; +} @@ -2630,189 +2440,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} -- --.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}} -- --.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}} -- --.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/client/room-invite-preview.js +var INVITE_PREVIEW_NAME_MAX = 60; +function stripInviteMarkers(text2) { @@ -2893,79 +2522,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-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/approval-claims.js +var claims = /* @__PURE__ */ new Map(); +var claimListeners = /* @__PURE__ */ new Set(); @@ -2991,77 +2548,13 @@ diff --git a/client.js b/client.js + claimListeners.delete(fn); + }; +} - --.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} ++ +// src/shared/approval-waiting.js +var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]); +function isApprovalWaiting(status) { + return WAITING_STATUSES.includes(status); +} - --.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} ++ +// 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). @@ -3081,41 +2574,7 @@ diff --git a/client.js b/client.js + const said = `${record.error ?? ""} ${record.code ?? ""}`; + return !TERMINAL.some((reason) => reason.test(said)); +}; - --.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} ++ +// src/client/flybot-slots.js +var import_react2 = require("react"); +var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload); @@ -3409,57 +2868,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; } - --/* 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 @@ -3488,66 +2897,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 = (text2) => { + const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text2); @@ -3574,21 +2924,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) => { @@ -3601,15 +2937,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 }) { @@ -3639,66 +2967,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 = { @@ -3803,114 +3072,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; @@ -3937,270 +3099,115 @@ 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} --.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} ++ +// src/client/motion.js +var import_react4 = require("react"); -+var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; -+function springEasing(stiffness, damping) { -+ const w0 = Math.sqrt(stiffness); -+ const zeta = damping / (2 * w0); -+ const at = (t2) => { -+ if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); -+ const wd = w0 * Math.sqrt(1 - zeta * zeta); -+ return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); -+ }; -+ let end = 0.05; -+ while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; -+ const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); -+ return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; -+} -+var SPRING_SHAPE = springEasing(400, 31.2); -+var SPRING_TAP = springEasing(900, 48); -+var SPRING_LIST = springEasing(484, 35.2); -+var SPRING_STRETCH = springEasing(320, 20); -+var SPRING_POP = springEasing(500, 22); -+var SPRING_LEAD = springEasing(784, 48.2); -+var SPRING_TRAIL = springEasing(289, 29.2); -+var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; -+var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); -+function liveSprings(apply2) { -+ const values = /* @__PURE__ */ new Map(); -+ let frame = 0; -+ let last = 0; -+ const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); -+ const step = (now) => { -+ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); -+ last = now; -+ const steps = Math.ceil(dt * 240); + var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; + function springEasing(stiffness, damping) { + const w0 = Math.sqrt(stiffness); +@@ -1659,12 +1659,12 @@ function liveSprings(apply2) { + const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); + last = now; + const steps = Math.ceil(dt * 240); +- const h34 = dt / steps; + const h50 = dt / steps; -+ let moving = false; -+ for (const value of values.values()) { -+ for (let index = 0; index < steps; index += 1) { + let moving = false; + for (const value of values.values()) { + for (let index = 0; index < steps; index += 1) { +- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; +- value.x += value.v * h34; + value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h50; + value.x += value.v * h50; -+ } -+ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { -+ value.x = value.to; -+ value.v = 0; -+ } else moving = true; -+ } -+ apply2(read()); -+ frame = moving ? requestAnimationFrame(step) : 0; -+ }; -+ return { -+ target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, -+ set(targets, curves, jump) { -+ for (const [key, to] of Object.entries(targets)) { -+ const value = values.get(key); -+ const { stiffness: k, damping: c } = curves[key]; -+ if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); -+ else Object.assign(value, { to, k, c }); -+ } -+ if (jump) { -+ cancelAnimationFrame(frame); -+ frame = 0; -+ apply2(read()); -+ return; -+ } -+ if (!frame) { -+ last = performance.now(); -+ frame = requestAnimationFrame(step); -+ } -+ }, -+ stop: () => cancelAnimationFrame(frame) -+ }; -+} -+var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; -+function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { + } + if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { + value.x = value.to; +@@ -1699,10 +1699,10 @@ function liveSprings(apply2) { + } + var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; + function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { +- const ownBox = (0, import_react.useRef)(null); + const ownBox = (0, import_react4.useRef)(null); -+ const box = given ?? ownBox; + const box = given ?? ownBox; +- const thumb = (0, import_react.useRef)(null); +- const own = (0, import_react.useRef)(null); + const thumb = (0, import_react4.useRef)(null); + const own = (0, import_react4.useRef)(null); -+ if (own.current === null) { -+ own.current = { -+ key: void 0, -+ node: null, -+ observer: null, -+ springs: liveSprings(({ start, end }) => { -+ const knob = thumb.current; -+ if (!knob) return; -+ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; -+ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; -+ }) -+ }; -+ } -+ const place = (jump) => { -+ const container = box.current; -+ const knob = thumb.current; -+ if (!container || !knob) return; -+ const chosen = container.querySelector(selector); -+ if (!chosen) { -+ knob.dataset.hidden = ""; -+ return; -+ } -+ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; -+ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); -+ const { springs } = own.current; -+ const before = springs.target(); -+ const hidden = knob.dataset.hidden !== void 0; -+ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; -+ delete knob.dataset.hidden; -+ const forward = before === null || start >= before.start; -+ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); -+ container.dataset.glide = ""; -+ }; + if (own.current === null) { + own.current = { + key: void 0, +@@ -1736,7 +1736,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai + springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); + container.dataset.glide = ""; + }; +- (0, import_react.useLayoutEffect)(() => { + (0, import_react4.useLayoutEffect)(() => { -+ const state = own.current; -+ const node = box.current; -+ if (node === state.node && key === state.key) return; -+ const fresh = node !== state.node; -+ state.key = key; -+ if (fresh) { -+ state.observer?.disconnect(); -+ state.node = node; -+ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; -+ state.observer?.observe(node); -+ } -+ place(fresh); -+ }); + const state = own.current; + const node = box.current; + if (node === state.node && key === state.key) return; +@@ -1750,7 +1750,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai + } + place(fresh); + }); +- (0, import_react.useEffect)(() => () => { + (0, import_react4.useEffect)(() => () => { -+ own.current.observer?.disconnect(); -+ own.current.springs.stop(); -+ }, []); -+ return { box, thumb }; -+} -+function Segmented({ label, value, className, children }) { -+ const glide = useGlide(value); + own.current.observer?.disconnect(); + own.current.springs.stop(); + }, []); +@@ -1758,16 +1758,16 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai + } + function Segmented({ label, value, className, children }) { + const glide = useGlide(value); +- return (0, import_react.createElement)( + return (0, import_react4.createElement)( -+ "div", -+ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, + "div", + { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, +- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ children -+ ); -+} -+function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + children + ); + } + function useFlip(list, key, selector, id = (node) => node.dataset.flip) { +- const tops = (0, import_react.useRef)(null); +- (0, import_react.useLayoutEffect)(() => { + const tops = (0, import_react4.useRef)(null); + (0, import_react4.useLayoutEffect)(() => { -+ const container = list.current; -+ if (!container) return; -+ const next = /* @__PURE__ */ new Map(); -+ const moving = tops.current !== null && !reducedMotion(); -+ for (const node of container.querySelectorAll(selector)) { -+ const name = id(node); -+ const top = node.offsetTop; -+ next.set(name, top); -+ if (!moving) continue; -+ const before = tops.current.get(name); -+ if (before === void 0) { -+ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); -+ } else if (Math.abs(before - top) > 0.5) { -+ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); -+ } -+ } -+ tops.current = next; -+ }, [key]); -+} -+function useMorph(box, key) { + const container = list.current; + if (!container) return; + const next = /* @__PURE__ */ new Map(); +@@ -1788,8 +1788,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + }, [key]); + } + function useMorph(box, key) { +- const last = (0, import_react.useRef)({ key, height: null }); +- (0, import_react.useLayoutEffect)(() => { + const last = (0, import_react4.useRef)({ key, height: null }); + (0, import_react4.useLayoutEffect)(() => { -+ const node = box.current; -+ const state = last.current; -+ if (!node) { -+ last.current = { key, height: null }; -+ return; -+ } -+ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); -+ if (key === state.key) { -+ if (running.length === 0) state.height = node.offsetHeight; -+ return; -+ } -+ const from = running.length ? node.offsetHeight : state.height; -+ for (const animation2 of running) animation2.cancel(); -+ const to = node.offsetHeight; -+ last.current = { key, height: to }; -+ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; -+ node.dataset.morph = ""; -+ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); -+ animation.onfinish = () => { -+ delete node.dataset.morph; -+ }; -+ animation.oncancel = () => { -+ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; -+ }; -+ }); -+} -+function useLinger(value, ms) { + const node = box.current; + const state = last.current; + if (!node) { +@@ -1817,8 +1817,8 @@ function useMorph(box, key) { + }); + } + 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} @@ -4209,83 +3216,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} @@ -4294,325 +3232,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-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 @@ -4621,8 +3365,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 @@ -4630,68 +3374,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,63 @@ button.bt-set-row:hover{background:var(--bt-hover)} + .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} + .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} + .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} +/* #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)} @@ -4736,331 +3446,81 @@ 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} - .bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} -@@ -2595,2185 +2595,4658 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} - :is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} - @media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} + .bt-mem-scrim{animation:bt-fade .2s ease-out} + .bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} + .bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box} +@@ -2597,9 +2597,20 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} `; --var CONVERSATION_CSS = ` --.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} + 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;max-width:100%;min-width:0;white-space:nowrap} -+.bt-think summary::-webkit-details-marker{display:none} + .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-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1} +.bt-think[open] .bt-think-caret{transform:rotate(90deg)} +.bt-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)} @@ -5073,27 +3533,27 @@ diff --git a/client.js b/client.js +.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;overflow-wrap:anywhere;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-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} + .bt-event>span{padding:0 2px} + .bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease} +@@ -2612,7 +2612,6 @@ 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 @@ -5113,9 +3573,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. @@ -5131,20 +3593,18 @@ 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,19 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} + [data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} + [data-chat-running]>span:not([role=status]){display:none!important} + [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} +.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} @@ -5153,224 +3613,212 @@ diff --git a/client.js b/client.js +.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s} +@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}} +@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} -+.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)} -+`; + .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} + .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} + .bt-ububble p{margin:0;line-height:22px} +@@ -2784,10 +2784,10 @@ div:has(> nav [class*="_marks"]){display:none!important} + .bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} + .bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} + `; +-function installStyles(text, tag) { +function installStyles(text2, tag) { -+ const style = document.createElement("style"); -+ style.dataset.dshBot = tag; + const style = document.createElement("style"); + style.dataset.dshBot = tag; +- style.textContent = text; + style.textContent = text2; -+ 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; -+} + document.head.appendChild(style); + return style; + } +@@ -2807,9 +2807,9 @@ function sheetClassNames(sheet) { + } + return names; + } +-function compileFragmentSelectors(text, names) { +function compileFragmentSelectors(text2, names) { -+ const compiled = /* @__PURE__ */ new Map(); + const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { + return text2.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); -+ }); -+} + if (!compiled.has(fragment)) { + const exact = []; + for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +@@ -2818,7 +2818,7 @@ function compileFragmentSelectors(text, names) { + return compiled.get(fragment); + }); + } +-function installCompiledStyles(text, tag) { +function installCompiledStyles(text2, 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 style = installStyles("", tag); + const cache = /* @__PURE__ */ new WeakMap(); + const render = () => { +@@ -2832,7 +2832,7 @@ function installCompiledStyles(text, tag) { + } + for (const name of list) names.add(name); + } +- const next = compileFragmentSelectors(text, names); + const next = compileFragmentSelectors(text2, 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(); -+ }; -+} -+ + if (style.textContent !== next) style.textContent = next; + }; + render(); +@@ -2855,226 +2855,204 @@ 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]); +-} +-var FIDGETS = ["hop", "sway", "stretch", "look"]; +-function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { +- const look = given ?? lookOf(bot); +- const ref = (0, import_react2.useRef)(null); +- const maskId = (0, import_react2.useRef)(""); +- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; +- const [poked, setPoked] = (0, import_react2.useState)(false); +- useGaze(ref, gaze); +- const common = { +- ref, +- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", +- "data-shape": look.image ? "image" : shapeId(look.shape), +- "data-state": STATES.includes(state) ? state : "idle", +- "data-live": live ? "" : void 0, +- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, +- "data-gaze": gaze ? "" : void 0, +- "data-poke": pokeable ? "" : void 0, +- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, +- role: label ? "img" : void 0, +- "aria-label": label, +- "aria-hidden": label ? void 0 : true, +- onClick: pokeable ? () => setPoked(true) : void 0, +- onAnimationEnd: poked ? (event) => { +- if (/^bt-poke/.test(event.animationName)) setPoked(false); +- } : void 0 +- }; +- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); +- const box = shapeOf(look.shape).size; +- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); +-} +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) +- ); +-} +-function AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); +-} +-function MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); +-} +-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/shared/face-title.js +var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function isGenericFaceTitle(name) { @@ -5387,7 +3835,14 @@ diff --git a/client.js b/client.js +function isAutoFaceTitle(name) { + const n = String(name ?? "").trim(); + return isGenericFaceTitle(n) || n.startsWith("Face-to-face · "); -+} + } +-function clusterSeats(count, frame) { +- if (count === 1) return [[0, 0, frame]]; +- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; +- if (count === 3) { +- const size = frame * 5 / 9; +- const rest = frame - size; +- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; +function ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -5398,10 +3853,43 @@ diff --git a/client.js b/client.js + seen.add(key); + const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase(); + rows.push({ owner, sort: handle || key }); -+ } + } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; + rows.sort((a, b) => a.sort.localeCompare(b.sort)); + return rows.slice(0, 3).map((row) => row.owner); -+} + } +-function RoomAvatar({ room: room2, roster, size = 36 }) { +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const people = members.length === 1 ? [members[0], null] : members; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); +- } +- const seats = clusterSeats(Math.min(people.length, 4), size); +- const overflow = people.length > 4 ? people.length - 3 : 0; +- const gap = size / 14; +- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-cluster", style: { width: size, height: size } }, +- seats.map(([x, y, s], index) => { +- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); +- const style = { left: x, top: y, width: s, height: s }; +- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); +- if (overflow > 0 && index === 3) { +- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); +- } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) +- ); +function uniqueOwners(owners) { + const seen = /* @__PURE__ */ new Set(); + const out = []; @@ -5506,116 +3994,135 @@ diff --git a/client.js b/client.js + observer?.disconnect(); + link.remove(); + }; -+} -+ -+// src/client/icons.js + } + + // src/client/icons.js +-var import_react3 = require("react"); +var import_react5 = require("react"); -+function PlusIcon() { + 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" })); + return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} -+function SearchIcon() { + } + 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" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} -+function PeopleIcon() { + } + function PeopleIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, + "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" }) + (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), + (0, import_react5.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), + (0, import_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) -+ ); -+} -+function ArrowUpIcon() { + ); + } + 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" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); -+} -+function MicIcon({ size = 16 }) { + } + function MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, + "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" }) + (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), + (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -+ ); -+} -+function WaveIcon({ size = 16 }) { + ); + } + function WaveIcon({ size = 16 }) { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + "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 })) + [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) -+ ); -+} -+function ShareIcon() { + ); + } + function ShareIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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" }) + (0, import_react5.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) -+ ); -+} -+function CloseIcon() { + ); + } + 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" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} + } +-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); +var ChevronLeftIcon = () => (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +var ChevronRightIcon = () => (0, import_react5.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); -+var lineIcon = (paths) => function LineIcon() { + var lineIcon = (paths) => function LineIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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 })) + paths.map((d, index) => (0, import_react5.createElement)("path", { key: index, d })) -+ ); -+}; -+var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; -+function CloudDownloadIcon({ done = false }) { + ); + }; + 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)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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" })) + (0, import_react5.createElement)("path", { d: CLOUD }), + done ? (0, import_react5.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react5.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react5.createElement)("path", { d: "M10 9.75v7" }), (0, import_react5.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -+ ); -+} -+var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); -+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 SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +@@ -3086,6 +3086,7 @@ var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]) + var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); + var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); + var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +var WorkflowIcon = lineIcon(["M4.5 4h4v3.5h-4z", "M11.5 12.5h4V16h-4z", "M6.5 7.5v3.5a2 2 0 0 0 2 2h3"]); -+var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); -+var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); -+var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); -+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() { + var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); + var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); + var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +@@ -3093,14 +3093,14 @@ var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 + 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)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( + (0, import_react5.createElement)( -+ "g", -+ { transform: "rotate(45 10 10)" }, + "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" }) + (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" }) -+ ) -+ ); -+} -+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"]); + ) + ); + } +@@ -3112,21 +3112,48 @@ 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 + var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); + var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); + var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +var LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]); -+var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); -+var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); -+var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); + var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); + var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); + var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +var ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]); +var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]); +var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]); -+var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); -+var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; + var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); + var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +function FlyBotMark({ size = 24 }) { + return (0, import_react5.createElement)( + "svg", @@ -5638,980 +4145,671 @@ diff --git a/client.js b/client.js + (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }), + (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 }) + ); - } --@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; -+ -+// src/client/questions.js ++} + + // src/client/questions.js +-var import_react4 = require("react"); +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) { + 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); + const pick2 = (0, import_react6.useRef)(onPick); -+ pick2.current = onPick; + pick2.current = onPick; +- (0, import_react4.useEffect)(() => { + (0, import_react6.useEffect)(() => { -+ if (count === 0) return void 0; -+ const onKey = (event) => { -+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -+ if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -+ if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -+ const index = event.key.toUpperCase().charCodeAt(0) - 65; -+ if (index >= 0 && index < count) { -+ event.preventDefault(); -+ pick2.current(index); -+ } else if (index === count && ownRef.current) { -+ event.preventDefault(); -+ ownRef.current.focus(); -+ } -+ }; -+ document.addEventListener("keydown", onKey, true); -+ return () => document.removeEventListener("keydown", onKey, true); -+ }, [count, ownRef]); + if (count === 0) return void 0; + const onKey = (event) => { + if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +@@ -3146,10 +3146,10 @@ function useOptionKeys(count, onPick, ownRef) { + }, [count, ownRef]); } --var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; --var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; --function sheetClassNames(sheet) { -- const names = []; -- const visit = (rules) => { -- for (const rule of rules) { -- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); -- if (rule.cssRules) visit(rule.cssRules); -+function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { + 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 [own, setOwn] = (0, import_react6.useState)(""); + const [picked, setPicked] = (0, import_react6.useState)([]); + const ownRef = (0, import_react6.useRef)(null); + const titleId = (0, import_react6.useId)(); -+ const options = optionsOf(question); -+ const multi = question.multiSelect === true; -+ const custom = own.trim(); -+ const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -+ const choose = (label) => { -+ if (multi) { -+ setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); -+ return; -+ } -+ onAnswer({ selected: [label] }); -+ }; -+ const submit = () => { -+ if (!canSubmit) return; -+ onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -+ }; -+ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -+ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + const options = optionsOf(question); + const multi = question.multiSelect === true; + const custom = own.trim(); +@@ -3167,7 +3167,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised + }; + useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); + const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { + const field = ownAnswer ? (0, import_react6.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); + ref: ownRef, + rows: 1, + value: own, +@@ -3186,7 +3186,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised + }) : null; + const rows = options.map((option, optionIndex) => { + const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( + return (0, import_react6.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) -+ }, + "button", + { + key: option.label, +@@ -3196,36 +3196,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised + "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)( + (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), + (0, import_react6.createElement)( -+ "span", -+ { className: "bt-q-body" }, + "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 + (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label), + option.description ? (0, import_react6.createElement)("span", { className: "bt-q-desc" }, option.description) : null -+ ), + ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null + selected && !raised ? (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) : null -+ ); -+ }); -+ if (raised && field) { + ); + }); + if (raised && field) { +- rows.push((0, import_react4.createElement)( + rows.push((0, import_react6.createElement)( -+ "label", -+ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, + "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) + ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, field) -+ )); -+ } -+ let foot = null; -+ if (raised && multi) { + )); + } + let foot = null; + if (raised && multi) { +- foot = (0, import_react4.createElement)( + foot = (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-foot" }, + "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")) + (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) { + ); + } 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"))); + foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); -+ } + } +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "form", -+ { -+ className: raised ? "bt-q bt-q-raised" : "bt-q", -+ "aria-labelledby": titleId, -+ onSubmit: (event) => { -+ event.preventDefault(); -+ submit(); -+ } -+ }, + "form", + { + className: raised ? "bt-q bt-q-raised" : "bt-q", +@@ -3235,24 +3235,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised + submit(); + } + }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "div", + { className: "bt-q-head" }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-text" }, + "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_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), + question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, + raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null -+ ), + ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon)) -+ ), + ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( + rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, + field && !raised ? (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-custom" }, + "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 + (0, import_react6.createElement)("label", { className: "bt-q-field" }, field), + !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -+ ) : null, -+ foot -+ ); -+} -+function SettledQuestion({ question, answer }) { -+ const options = optionsOf(question); -+ const prompt = question?.question ?? ""; -+ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -+ if (chosen.length === 0) { + ) : null, + foot + ); +@@ -3262,29 +3262,29 @@ function SettledQuestion({ question, answer }) { + const prompt = question?.question ?? ""; + const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; + if (chosen.length === 0) { +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, + "div", + { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "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")) + (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)), + (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -+ ) -+ ); -+ } + ) + ); + } +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, + "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) => { + prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null, + (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -+ const optionIndex = options.findIndex((option) => option.label === label); + const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { key: `${label}-${index}`, className: "bt-q-opt" }, + "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)) + optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)), + (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) -+ ); -+ })) -+ ); -+} -+function QuestionCard({ matched }) { -+ const pending = matched; -+ const questions = pending.questions ?? []; + ); + })) + ); +@@ -3292,8 +3292,8 @@ function SettledQuestion({ question, answer }) { + 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 [index, setIndex] = (0, import_react6.useState)(0); + const [answers, setAnswers] = (0, import_react6.useState)([]); -+ const question = questions[index]; -+ if (question === void 0) return null; -+ const onAnswer = (answer) => { -+ const next = [...answers, { id: question.id, ...answer }]; -+ if (index + 1 < questions.length) { -+ setAnswers(next); -+ setIndex(index + 1); -+ return; + const question = questions[index]; + if (question === void 0) return null; + const onAnswer = (answer) => { +@@ -3305,10 +3305,10 @@ function QuestionCard({ matched }) { } -+ pending.answer({ answers: next }); + pending.answer({ answers: next }); }; -- try { -- visit(sheet.cssRules); -- } catch { +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); + return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); -+} -+function PendingQuestion({ sessionId, question, actions, inline }) { + } + function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); + const [answered, setAnswered] = (0, import_react6.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); -+ }; + if (answered === question.id) return null; + const onAnswer = (answer) => { + setAnswered(question.id); +@@ -3318,10 +3318,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) { + setAnswered(question.id); + void actions.dismissQuestion(sessionId); + }; +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: inline ? "bt-q-inline" : "bt-q-dock" }, + "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 }) + (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) -+ ); -+} -+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; + ); + } + function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { +@@ -3329,16 +3329,16 @@ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { + 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 }); + return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); -+} -+function AnsweredCell({ node }) { -+ const data = node.data ?? {}; -+ const questions = Array.isArray(data.questions) ? data.questions : []; -+ if (questions.length === 0) { + } + function AnsweredCell({ node }) { + const data = node.data ?? {}; + const questions = Array.isArray(data.questions) ? data.questions : []; + if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; + return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null; } -- return names; -+ const answers = Array.isArray(data.answers) ? data.answers : []; + 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, { + return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, { -+ key: question.id, -+ question, -+ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -+ }))); + key: question.id, + question, + answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +@@ -3346,37 +3346,37 @@ function AnsweredCell({ node }) { } --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); -+ -+// src/client/secret-card.js + + // src/client/secret-card.js +-var import_react5 = require("react"); +var import_react7 = require("react"); -+var MIN_VALUE = 4; -+var NONE = {}; + var MIN_VALUE = 4; + var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +var KeyIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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" }) + (0, import_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), + (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) -+); + ); +-var LockIcon = () => (0, import_react5.createElement)( +var LockIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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 }) + (0, import_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), + (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) -+); + ); +-var WarnIcon = () => (0, import_react5.createElement)( +var WarnIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "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" }) + (0, import_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), + (0, import_react7.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) -+); -+var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); -+var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); -+var scopeLabel = (scope, roster) => { + ); + 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"); + if (scope === "all") return t("All my Bots"); -+ const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -+ return names.length === 0 ? t("No Bot") : names.join(t(" and ")); -+}; -+var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; -+function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { + 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", { + return (0, import_react7.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?.(); - } -- return compiled.get(fragment); + className: "bt-secret-input", + type: MASK_BY_CSS ? "text" : "password", + "data-masked": MASK_BY_CSS ? "" : void 0, +@@ -3401,42 +3401,42 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { }); } --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(); -- }; -+function ScopeSwitch({ value, onChange, bot }) { + function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, + "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", { + (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), + options.map(([id, text2]) => (0, import_react7.createElement)("button", { -+ key: id, -+ type: "button", -+ role: "radio", -+ "aria-checked": value === id, -+ className: "bt-secret-seg", -+ onClick: () => onChange(id) + key: id, + type: "button", + role: "radio", + "aria-checked": value === id, + className: "bt-secret-seg", + onClick: () => onChange(id) +- }, text)) + }, text2)) -+ ); + ); } -- --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); -- }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); -- }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); -- return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); -- }; -- }, [enabled]); -+function SecretHead({ bot, title, detail, name, onClose }) { + function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-head bt-secret-head" }, + "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)( + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-text" }, + "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}` : "") + (0, import_react7.createElement)("p", { className: "bt-q-title" }, title), + (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") -+ ), + ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null -+ ); + ); } --var FIDGETS = ["hop", "sway", "stretch", "look"]; --function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { -- const look = given ?? lookOf(bot); -- const ref = (0, import_react2.useRef)(null); -- const maskId = (0, import_react2.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; -- const [poked, setPoked] = (0, import_react2.useState)(false); -- useGaze(ref, gaze); -- const common = { -- ref, -- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", -- "data-shape": look.image ? "image" : shapeId(look.shape), -- "data-state": STATES.includes(state) ? state : "idle", -- "data-live": live ? "" : void 0, -- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, -- "data-gaze": gaze ? "" : void 0, -- "data-poke": pokeable ? "" : void 0, -- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, -- role: label ? "img" : void 0, -- "aria-label": label, -- "aria-hidden": label ? void 0 : true, -- onClick: pokeable ? () => setPoked(true) : void 0, -- onAnimationEnd: poked ? (event) => { -- if (/^bt-poke/.test(event.animationName)) setPoked(false); -- } : void 0 -+function SecretCard({ bot, request, roster, actions }) { + 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 [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); + const [replacing, setReplacing] = (0, import_react7.useState)(false); + const [busy, setBusy] = (0, import_react7.useState)(false); + const [error, setError] = (0, import_react7.useState)(""); + const noteId = (0, import_react7.useId)(); -+ const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -+ const run = (promise) => { -+ setBusy(true); -+ setError(""); -+ return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); + const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); + const run = (promise) => { + setBusy(true); +@@ -3452,73 +3452,73 @@ function SecretCard({ bot, request, roster, actions }) { + if (!busy) void run(actions.secretAllow(request.id)); }; -- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); -- const box = shapeOf(look.shape).size; -- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); -+ const cancel = () => run(actions.secretCancel(request.id)); -+ const long = value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -+ }; -+ const allow = () => { -+ if (!busy) void run(actions.secretAllow(request.id)); -+ }; -+ const filling = request.kind === "fill" || replacing; + const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( + const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, + "div", + { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { + (0, import_react7.createElement)(SecretHead, { -+ bot, -+ title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -+ detail: request.purpose, -+ name: request.name, -+ onClose: busy ? void 0 : cancel -+ }), + 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)( + request.kind === "allow" ? (0, import_react7.createElement)("div", { className: "bt-secret-saved" }, (0, import_react7.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, + filling ? (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-body" }, + "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 + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), + request.kind === "fill" ? (0, import_react7.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -+ ) : null, + ) : null, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-note", id: noteId }, -+ t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), + "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}`), + (0, import_react7.createElement)("code", null, `$DSH_SECRET_${request.name}`), -+ ".", -+ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" -+ ), -+ warn, + ".", + filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" + ), + warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( + error ? (0, import_react7.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-foot" }, + "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")) + request.kind === "allow" && !replacing ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, + (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), + filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) -+ ) -+ ); + ) + ); } --function StarIcon({ size = 8 }) { -- return (0, import_react2.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) -+function SettledSecret({ bot, request, roster }) { -+ const canceled = request.status === "canceled"; + 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)( + const text2 = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); + return (0, import_react7.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( + { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text2, "data-status": request.status }, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "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)( + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 22 })), + (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)( -+ "p", -+ { className: "bt-q-title" }, + "p", + { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) + text2, + canceled ? null : (0, import_react7.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -+ )), + )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) + canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) -+ ) + ) ); } --function AdminStar() { -- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); -+function RequestView({ bot, request, roster, actions }) { + 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 }); + return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster }); } --function MainStar() { -- return (0, import_react2.createElement)( -- "svg", -- { viewBox: "0 0 20 20", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) -- ); -+function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -+ const bot = roster.byId[sessionId]; -+ if (!bot) return null; -+ const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); + 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 (request) return (0, import_react7.createElement)("div", { className: "bt-q-inline" }, (0, import_react7.createElement)(RequestView, { bot, request, roster, actions })); -+ if (done && /already saved/.test(contentText(content))) { + if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); + return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -+ } -+ return null; + } + return null; } --function useSettled(state) { -- const prev = (0, import_react2.useRef)(state); -- const [settled, setSettled] = (0, import_react2.useState)(false); -- (0, import_react2.useEffect)(() => { -- const was = prev.current; -- prev.current = state; -- if (state !== "idle") { -- setSettled(false); -- return void 0; -- } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; -+function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { -+ const roster = useRoster((value) => value); -+ const shown = useCards?.((state) => state[sessionId]) ?? NONE; -+ const loading = useSession?.(historyLoading) === true; -+ const bot = roster.byId[sessionId]; -+ if (!bot || currentPart(bot) !== sessionId) return null; -+ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); -+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); -+ if (waiting.length === 0) return null; +@@ -3531,44 +3531,46 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { + const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); + const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); + if (waiting.length === 0) return null; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); + return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); } --function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { -- const settled = useSettled(state); -- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); -- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, -- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), -- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, -- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, -- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, -- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null +var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; -+function ScopeEditor({ secret, roster, onSave }) { + 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 [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); + const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope); -+ const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( + const next = mode === "all" ? "all" : mode === "none" ? [] : picked; + const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join(); + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit-block" }, + "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)( + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, + "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)( + [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text2]) => (0, import_react7.createElement)( -+ "label", -+ { key: id, className: "bt-secret-radio" }, + "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 + (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), + text2 -+ )) -+ ), + )) + ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { + mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.createElement)("button", { -+ key: bot.id, -+ type: "button", -+ className: "bt-secret-bot", -+ "aria-pressed": picked.includes(bot.id), -+ onClick: () => toggle(bot.id) + 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)( + }, (0, import_react7.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react7.createElement)(CheckIcon) : null))) : null, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, + "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")) + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) -+ ) + ) ); } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; -- } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; --} --function RoomAvatar({ room: room2, roster, size = 36 }) { -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const people = members.length === 1 ? [members[0], null] : members; -- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); -- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); -- if (size <= 20 && people.length > 1) { -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, -- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), -- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null -- ); -- } -- const seats = clusterSeats(Math.min(people.length, 4), size); -- const overflow = people.length > 4 ? people.length - 3 : 0; -- const gap = size / 14; -- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-cluster", style: { width: size, height: size } }, -- seats.map(([x, y, s], index) => { -- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); -- const style = { left: x, top: y, width: s, height: s }; -- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); -- if (overflow > 0 && index === 3) { -- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); -+function ValueEditor({ secret, onSave }) { + function ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); -+ const long = value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (!long) return; -+ void Promise.resolve(onSave(value)).then((ok) => { -+ if (ok) { -+ setValue(""); -+ setReplaced(true); - } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) -+ }); -+ }; -+ const edit = (next) => { -+ setValue(next); -+ setReplaced(false); -+ }; + const long = value.trim().length >= MIN_VALUE; + const save = () => { + if (!long) return; +@@ -3583,66 +3583,66 @@ function ValueEditor({ secret, onSave }) { + setValue(next); + setReplaced(false); + }; +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit-block" }, + "div", + { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-inline" }, + "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")) + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -+ ), + ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null + value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ); } -- --// src/client/icons.js --var import_react3 = require("react"); --function PlusIcon() { -- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); --} --function SearchIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+function SecretRow({ secret, roster, actions, run, open, onToggle }) { + function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { + const [confirm, setConfirm] = (0, import_react7.useState)(false); + (0, import_react7.useEffect)(() => { -+ if (!open) setConfirm(false); -+ }, [open]); -+ const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -+ const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -+ const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); + 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)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-item", "data-open": open ? "" : void 0 }, + "div", + { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, + "button", + { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "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_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta) -+ ), + ), +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "span", -+ { className: "bt-set-control" }, + "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 }))) + secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -+ ) -+ ), + ) + ), +- open ? (0, import_react5.createElement)( + open ? (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit" }, + "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)( + (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), + (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, -+ confirm ? [ + "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")) + (0, import_react7.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), + (0, import_react7.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), + (0, import_react7.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) + ] : (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete")) -+ ) -+ ) : null -+ ); + ) + ) : null + ); } --function 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 AddSecret({ actions, run, onDone }) { + 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 [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); + const [value, setValue] = (0, import_react7.useState)(""); -+ const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (!ready) return; -+ void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -+ if (ok) { -+ setValue(""); -+ onDone(); -+ } -+ }); -+ }; + const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; + const save = () => { + if (!ready) return; +@@ -3653,43 +3653,43 @@ function AddSecret({ actions, run, onDone }) { + } + }); + }; +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-set-row bt-set-paste bt-secret-add" }, + "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)( + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, + "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")) + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) -+ ) + ) ); } --function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); -+function SecretsPage({ roster, actions, run, Section }) { + function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); -+ const secrets = roster.secrets ?? []; -+ return [ + const secrets = roster.secrets ?? []; + return [ +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "p", -+ { key: "about", className: "bt-secret-about" }, -+ t("Keys your Bots asked for. Bots never see the values: they use them as "), + "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"), + (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"), -+ t(" in their shell, and every value is replaced by "), + t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), + (0, import_react7.createElement)("code", null, "[secret:NAME]"), -+ t(" before anything goes to a model.") -+ ), + t(" before anything goes to a model.") + ), +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ Section, -+ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, + 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 + secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, + secrets.map((secret) => (0, import_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), + adding ? (0, import_react7.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -+ ), + ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) + adding ? null : (0, import_react7.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -+ ]; -+} -+var SECRET_CSS = ` -+.bt-secret-head{align-items:center;gap:10px} -+.bt-secret-who{position:relative;flex:none;display:inline-flex} -+.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} -+.bt-secret-lock svg{width:11px;height:11px} -+.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} -+.bt-secret-body{display:flex;flex-direction:column;gap:8px} -+.bt-secret-field{align-items:center;min-height:36px} -+.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} -+.bt-secret-input[data-masked]{-webkit-text-security:disc} -+.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} -+.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} -+.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} -+.bt-secret-glide[data-at="1"]{transform:translateX(100%)} -+.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} -+.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} -+.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} -+/* A key card asks the user to act, so the process group carrying it stays open in every -+ work-details mode (same override as .bt-group-replies in styles.js). */ -+[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} -+[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} -+.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} -+.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} -+@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} -+.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} -+.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} -+.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} -+.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} -+.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} -+.bt-secret-done .bt-q-head{align-items:center} -+.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} -+.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} -+.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} -+.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} -+@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} -+@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} -+.bt-secret-event{display:inline-flex;align-items:center;gap:5px} -+.bt-secret-dock{flex-direction:column;align-items:center} -+.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} -+.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} -+.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} -+.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} -+.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} -+.bt-secret-faces{display:inline-flex} -+.bt-secret-faces>*+*{margin-left:-6px} -+.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} -+.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} -+.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} -+.bt-secret-radios{display:flex;gap:16px;font-size:13px} -+.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} -+.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} -+.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} -+.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} -+.bt-secret-bot svg{width:13px;height:13px} -+.bt-secret-inline{display:flex;gap:8px;align-items:center} -+.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} -+.bt-secret-row-actions .bt-set-hint{flex:1} -+.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} -+.bt-secret-danger svg{width:13px;height:13px} -+.bt-secret-add .bt-input{height:34px} -+@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} -+`; -+ + ]; + } + var SECRET_CSS = ` +@@ -3756,6 +3756,144 @@ var SECRET_CSS = ` + @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} + `; + +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -6666,22 +4864,10 @@ diff --git a/client.js b/client.js + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); + } + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; - } --function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -- ); ++} +function facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; - } --function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) ++} + +// src/client/held-reply.js +var import_react8 = require("react"); @@ -6754,123 +4940,70 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) + ), + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null - ); --} --function ShareIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) ++ ); + return (0, import_react8.createElement)( + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain - ); - } --function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+ -+// src/client/turn-reasoning.js -+function turnReasoning(steps, current) { -+ const members = steps.length ? steps : [current]; -+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); - } --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" })) -+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 + // src/client/turn-reasoning.js + function turnReasoning(steps, current) { + const members = steps.length ? steps : [current]; +@@ -3767,7 +3767,7 @@ var thinkingLive = (step) => step.status === "running" && texts(step).length === + 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_react9 = require("react"); -+var import_react_dom = require("react-dom"); -+var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); -+var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); -+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; + var import_react_dom = require("react-dom"); + var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); + var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); +@@ -3780,29 +3780,29 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, + actions.openSession(target); + }; + const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, + "div", + { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { + texts2.map((text2, index) => { -+ const key = msgKey ? `${msgKey}:${index}` : void 0; -+ const picked = key ? reactions?.[index] ?? [] : []; -+ const last = index === texts2.length - 1; -+ const live = streaming && last; + 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)( + return (0, import_react9.createElement)( -+ "div", -+ { key: index, className: "bt-msg" }, + "div", + { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "div", -+ { className: "bt-msg-line" }, -+ lead && last ? lead : null, + "div", + { className: "bt-msg-line" }, + lead && last ? lead : null, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "div", -+ { className: "bt-bubble" }, + "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 }) + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text2, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) -+ ), + ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) + live ? null : (0, import_react9.createElement)(MessageTools, { text: text2, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) -+ ), -+ picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.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 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" }) -- ) -- ); + ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { ++ picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { + key: emoji, + type: "button", + className: "bt-react", +@@ -3813,9 +3813,22 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, + }) + ); + } +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; -+var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; + var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text2) { + if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; + const match = String(text2 ?? "").trim().match(/^(.+) left(?: the room)?$/); @@ -6882,216 +5015,86 @@ diff --git a/client.js b/client.js + if (name && (who === name || who.endsWith(` (${name})`))) return true; + } + return false; - } --var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); --var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); --var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); --var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); --var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); --var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); --var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); --var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); --var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); --var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); --var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); --var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); --var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; -- --// src/client/questions.js --var import_react4 = require("react"); --var keyLetter = (index) => String.fromCharCode(65 + index); --var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); --var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; --function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); -- pick2.current = onPick; -- (0, import_react4.useEffect)(() => { -- if (count === 0) return void 0; -+function useDismiss(open, close, refs) { ++} + function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { + (0, import_react9.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.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -- const index = event.key.toUpperCase().charCodeAt(0) - 65; -- if (index >= 0 && index < count) { -- event.preventDefault(); -- pick2.current(index); -- } else if (index === count && ownRef.current) { -- event.preventDefault(); -- ownRef.current.focus(); -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ close(); - } + if (!open) return void 0; + const onDown = (event) => { + if (refs.some((ref) => ref.current?.contains(event.target))) return; +@@ -3835,12 +3835,12 @@ function useDismiss(open, close, refs) { }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); -+ }; -+ }, [open]); + }, [open]); } --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_react4.useState)(""); -- const [picked, setPicked] = (0, import_react4.useState)([]); -- const ownRef = (0, import_react4.useRef)(null); -- const titleId = (0, import_react4.useId)(); -- const options = optionsOf(question); -- const multi = question.multiSelect === true; -- const custom = own.trim(); -- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -- const choose = (label) => { -- if (multi) { -- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react9.useState)(null); + const [place, setPlace] = (0, import_react9.useState)(null); + const [copied, setCopied] = (0, import_react9.useState)(false); + const tools = (0, import_react9.useRef)(null); + const pop = (0, import_react9.useRef)(null); -+ const close = () => setPanel(null); -+ useDismiss(panel !== null, close, [tools, pop]); -+ const open = (kind, event) => { -+ if (panel === kind) { -+ close(); - return; - } -- onAnswer({ selected: [label] }); -+ const rect = event.currentTarget.getBoundingClientRect(); -+ const below = rect.bottom + 220 < window.innerHeight; -+ setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -+ setPanel(kind); + const close = () => setPanel(null); + useDismiss(panel !== null, close, [tools, pop]); + const open = (kind, event) => { +@@ -3854,36 +3854,36 @@ function MessageTools({ text, name, sessionId, react, actions }) { + setPanel(kind); }; -- const submit = () => { -- if (!canSubmit) return; -- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -+ const copy2 = () => { + const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { + void navigator.clipboard?.writeText(text2).then(() => { -+ setCopied(true); -+ setTimeout(() => setCopied(false), 1e3); -+ }); + setCopied(true); + setTimeout(() => setCopied(false), 1e3); + }); }; -- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { -- ref: ownRef, -- rows: 1, -- value: own, -- autoComplete: "off", -- spellCheck: false, -- className: "bt-q-input", -- "aria-label": t("Your own answer"), -- placeholder: t("Type your own answer"), -- "aria-keyshortcuts": ownLetter?.toLowerCase(), -- onChange: (event) => setOwn(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- submit(); -- } -- }) : null; -- const rows = options.map((option, optionIndex) => { -- const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( -- "button", -- { -- key: option.label, -- type: "button", -- className: "bt-q-opt", -- "aria-pressed": multi ? selected : void 0, -- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -- onClick: () => choose(option.label) -- }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( -- "span", -- { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null -- ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null -- ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); -- } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( -+ const download2 = () => { + const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); -+ const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -+ link.click(); -+ setTimeout(() => URL.revokeObjectURL(url), 1e3); -+ }; + const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1e3); + }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -+ close(); -+ run(); + close(); + run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( + } }, (0, import_react9.createElement)(Icon), label); + return (0, import_react9.createElement)( -+ "div", -+ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + "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)( + react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, + (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) -- ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); -- } -- return (0, import_react4.createElement)( -- "form", -- { -- className: raised ? "bt-q bt-q-raised" : "bt-q", -- "aria-labelledby": titleId, -- onSubmit: (event) => { -- event.preventDefault(); -- submit(); -- } -- }, -- (0, import_react4.createElement)( -+ { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + { 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: () => { + REACTIONS.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -+ close(); -+ react(emoji); -+ } }, emoji)) -+ ), document.body) : null, + close(); + react(emoji); + } }, emoji)) + ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( + panel === "more" ? (0, import_react_dom.createPortal)((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)) -+ { 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 -+ ); -+} + { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, + item(t("Copy"), CopyIcon, copy2), +@@ -3891,12 +3891,40 @@ function MessageTools({ text, name, sessionId, react, actions }) { + ), document.body) : null + ); + } +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -7116,42 +5119,31 @@ diff --git a/client.js b/client.js + if (typeof text2 !== "string" || text2.trim() === "") return null; + const peek = peekLine(text2); + return (0, import_react9.createElement)( -+ "details", -+ { className: "bt-think" }, + "details", + { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) + (0, import_react9.createElement)( + "summary", + null, + (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), + t("Thought it through"), + peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( -- "div", -- { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -- ) : null, -- foot ++ ), + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) ); } --function SettledQuestion({ question, answer }) { -- const options = optionsOf(question); -- const prompt = question?.question ?? ""; -- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -- if (chosen.length === 0) { -- return (0, import_react4.createElement)( -+function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -+ const tail = useTurnData?.("turn-tail"); -+ const location2 = node.location; -+ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -+ const seq = node.data.finalNode?.seq; + function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +@@ -3904,19 +3904,33 @@ function useFileMentions({ node, openFile, fileMentions, useTurnData }) { + 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)(() => { + return (0, import_react9.useMemo)(() => { -+ if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -+ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -+ }, [fileMentions, openFile, seq, tail, turn]); -+} + if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; + return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; + }, [fileMentions, openFile, seq, tail, turn]); + } +function useClosingText(node, useTurnData) { + const tail = useTurnData?.("turn-tail"); + const seq = node.data.finalNode?.seq; @@ -7163,41 +5155,45 @@ diff --git a/client.js b/client.js + if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; + return (0, import_react9.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react9.createElement)(BotMark, { bot, size: 22 })); +} -+function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -+ const roster = useRoster((value) => value); -+ const data = node.data; -+ const steps = useTurnSteps(String(data.turn)); -+ const contexts = useTurnContext(String(data.turn)); -+ const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); + function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { + const roster = useRoster((value) => value); + const data = node.data; + const steps = useTurnSteps(String(data.turn)); + const contexts = useTurnContext(String(data.turn)); + const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); + const held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn); + const closing = useClosingText(node, useTurnData); -+ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); + const 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 person = typedBy(roster, sessionId, data.turn); + const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text2) => text2.trim() !== ""); -+ const running = data.status === "running"; -+ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); -+ if (hidden) return null; -+ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -+ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; -+ if (texts2.length === 0 && !reasoning && relays.length === 0) return null; -+ const selfId = roster.byId[sessionId]?.id ?? sessionId; + const running = data.status === "running"; + useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); + if (hidden) return null; +@@ -3924,13 +3924,14 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use + const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; + if (texts2.length === 0 && !reasoning && relays.length === 0) return null; + const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, + const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null; + return (0, import_react9.createElement)( + import_react9.Fragment, -+ null, -+ relays.map((relay) => { -+ const source = relay.source; -+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + 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, { + return (0, import_react9.createElement)(EventLine, { -+ key: relay.seq, -+ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -+ bots: [sender], -+ onOpen: () => { -+ if (sender.id) actions.openExchange(selfId, sender.id); -+ }, -+ whole: true -+ }); -+ }), + key: relay.seq, + prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), + bots: [sender], +@@ -3940,61 +3940,230 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use + 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 + reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles + ); @@ -7220,42 +5216,47 @@ diff --git a/client.js b/client.js + { className: "bt-group-msg bt-person-msg", "data-person": name }, + (0, import_react9.createElement)("span", { className: "bt-group-name" }, name), + (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead }) -+ ); -+} -+function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -+ if (whole && bots.length === 1) { + ); + } + function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { + if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( "div", -- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -+ { className: "bt-event" }, + { className: "bt-event" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, + "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) + (0, import_react9.createElement)("span", null, prefix), + (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name) ) ); } -- return (0, import_react4.createElement)( -+ const parts = []; + const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); + parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix)); -+ bots.forEach((bot, index) => { + bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( + if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner)); + parts.push((0, import_react9.createElement)( -+ "button", -+ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, + "button", + { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), -+ bot.name -+ )); -+ }); + bot.name + )); + }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts); -+} + } +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); +var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); +function isHeldLine(room2, text2, bot) { @@ -7292,10 +5293,12 @@ diff --git a/client.js b/client.js + } + if (held === "held" || isHeldLine(room2, text2, bot)) return null; + const lead = (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, + "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 }) + (0, import_react9.createElement)(BotMark, { bot, size: 22 }) -+ ); + ); +- return (0, import_react6.createElement)( + const name = (0, import_react9.createElement)( + "span", + { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, @@ -7304,53 +5307,12 @@ diff --git a/client.js b/client.js + ); + return (0, import_react9.createElement)( "div", -- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -- const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( -- "div", -- { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) -- ); -- })) -+ { className: "bt-group-msg" }, + { className: "bt-group-msg" }, +- (0, import_react6.createElement)( + name, + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) - ); - } --function QuestionCard({ matched }) { -- const pending = matched; -- const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); -- const question = questions[index]; -- if (question === void 0) return null; -- const onAnswer = (answer) => { -- const next = [...answers, { id: question.id, ...answer }]; -- if (index + 1 < questions.length) { -- setAnswers(next); -- setIndex(index + 1); -- return; -- } -- pending.answer({ answers: next }); -- }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); --} --function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); -- if (answered === question.id) return null; -- const onAnswer = (answer) => { -- setAnswered(question.id); -- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); -- }; -- const onDismiss = () => { -- setAnswered(question.id); -- void actions.dismissQuestion(sessionId); -- }; -- return (0, import_react4.createElement)( ++ ); ++} +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -7407,45 +5369,28 @@ diff --git a/client.js b/client.js + const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react9.createElement)( - "div", -- { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ++ "div", + { className: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react9.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react9.createElement)( -+ "span", -+ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, + "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 + name, + (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle) -+ ), + ), +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null - ); - } --function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const question = useRoster((value) => value.questions?.[sessionId]); -- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; -- const loading = useSession?.(historyLoading) === true; -- if (question === void 0 || question.callId && (drawn || loading)) return null; -- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); ++ ); ++} +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; + const bot = roster.byId[sessionId]?.name ?? t("your Bot"); + return (0, import_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot }))); - } --function AnsweredCell({ node }) { -- const data = node.data ?? {}; -- const questions = Array.isArray(data.questions) ? data.questions : []; -- if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; -- } -- const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { -- key: question.id, -- question, -- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -- }))); ++} +function GlobeMark() { + return (0, import_react9.createElement)( + "svg", @@ -7453,60 +5398,7 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ); - } -- --// src/client/secret-card.js --var import_react5 = require("react"); --var MIN_VALUE = 4; --var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) --); --var LockIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) --); --var WarnIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) --); --var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); --var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); --var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); -- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); --}; --var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; --function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { -- return (0, import_react5.createElement)("input", { -- className: "bt-secret-input", -- type: MASK_BY_CSS ? "text" : "password", -- "data-masked": MASK_BY_CSS ? "" : void 0, -- value, -- autoFocus, -- autoComplete: MASK_BY_CSS ? "off" : "new-password", -- autoCorrect: "off", -- autoCapitalize: "off", -- spellCheck: false, -- "data-1p-ignore": "", -- "data-lpignore": "true", -- "data-bwignore": "", -- "data-form-type": "other", -- "aria-label": label, -- placeholder: t("Paste or type the key"), -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- onEnter?.(); ++} +function RemoteFiles({ files, plugin, botId }) { + const [busy, setBusy] = (0, import_react9.useState)(""); + const [error, setError] = (0, import_react9.useState)(""); @@ -7525,25 +5417,10 @@ diff --git a/client.js b/client.js + setError(String(reason?.message ?? reason)); + } finally { + setBusy(""); - } -- }); --} --function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( ++ } + }; + return (0, import_react9.createElement)( - "div", -- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, -- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -- options.map(([id, text]) => (0, import_react5.createElement)("button", { -- key: id, -- type: "button", -- role: "radio", -- "aria-checked": value === id, -- className: "bt-secret-seg", -- onClick: () => onChange(id) -- }, text)) ++ "div", + { className: "bt-file-chips" }, + files.map((file) => (0, import_react9.createElement)( + "button", @@ -7551,313 +5428,231 @@ diff --git a/client.js b/client.js + `📎 ${file.name} · ${fileSize(file.size)}` + )), + error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null -+ ); -+} -+function GroupEvent({ prefix, room: room2, name, roster, actions }) { + ); + } + function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-event" }, + "div", + { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null + (0, import_react9.createElement)("span", null, prefix), + room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null ); } --function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( -+function 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 }); + function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +@@ -4005,14 +4005,14 @@ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { + 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)( + return (0, import_react9.createElement)( "div", -- { className: "bt-q-head bt-secret-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-text" }, -- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), -- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") -- ), -- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null -+ { className: "bt-event" }, + { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) ); } --function SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); -- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -- const run = (promise) => { -- setBusy(true); -- setError(""); -- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -- }; -- const cancel = () => run(actions.secretCancel(request.id)); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -- }; -- const allow = () => { -- if (!busy) void run(actions.secretAllow(request.id)); -+function PartLine({ time }) { + 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); + return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null); -+} -+var scrollParent = (node) => { -+ for (let at = node.parentElement; at; at = at.parentElement) { -+ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -+ } -+ return null; -+}; -+function EarlierParts({ sessionId, roster, actions }) { + } + var scrollParent = (node) => { + for (let at = node.parentElement; at; at = at.parentElement) { +@@ -4021,11 +4021,11 @@ var scrollParent = (node) => { + return null; + }; + function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); + const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react9.useRef)(null); + const busy = (0, import_react9.useRef)(false); + const alive = (0, import_react9.useRef)(true); + const restore = (0, import_react9.useRef)(null); -+ const load = (cursor) => { -+ if (busy.current) return; -+ busy.current = true; -+ 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 })); -+ }); + const load = (cursor) => { + if (busy.current) return; + busy.current = true; +@@ -4041,19 +4041,19 @@ function EarlierParts({ sessionId, roster, actions }) { + if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); + }); }; -- const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ alive.current = true; -+ load(void 0); -+ return () => { -+ alive.current = false; -+ }; -+ }, [sessionId]); + alive.current = true; + load(void 0); + return () => { + alive.current = false; + }; + }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ const pending = restore.current; -+ restore.current = null; -+ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -+ }, [view.items]); + const pending = restore.current; + restore.current = null; + if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; + }, [view.items]); +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ const node = top.current; -+ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -+ const observer = new IntersectionObserver((entries2) => { -+ if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -+ }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -+ observer.observe(node); -+ return () => observer.disconnect(); -+ }, [view.cursor, view.loading, view.failed]); -+ const selfId = roster.byId[sessionId]?.id; -+ const rows = []; -+ for (const item of view.items) { -+ const last = rows.at(-1); -+ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -+ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -+ } + const node = top.current; + if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; + const observer = new IntersectionObserver((entries2) => { +@@ -4069,35 +4069,35 @@ function EarlierParts({ sessionId, roster, actions }) { + if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); + else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); + } +- return (0, import_react6.createElement)( + return (0, import_react9.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)( -+ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, + { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( + (0, import_react9.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 }) : "" -+ { ref: top, className: "bt-earlier-top" }, + { ref: top, className: "bt-earlier-top" }, +- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null + 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 ), -- 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) { -+ case "divider": + rows.map((row, index) => { + const key = rows.length - index; + switch (row.kind) { + case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); + return (0, import_react9.createElement)(PartLine, { key, time: row.time }); -+ case "user": + case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( + return (0, import_react9.createElement)("div", { key, className: "bt-urow" }, (0, import_react9.createElement)( -+ "div", -+ { className: "bt-ububble" }, + "div", + { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null + row.text ? (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, + row.images ? (0, import_react9.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -+ )); -+ case "answer": + )); + case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); + return (0, import_react9.createElement)(AnsweredCell, { key, node: { data: row } }); -+ case "bot": + case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); + return (0, import_react9.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -+ case "event": { + case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); + if (row.role === "report") return (0, import_react9.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -+ const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -+ const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); + const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; + const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { + return (0, import_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -+ if (selfId && other.id) actions.openExchange(selfId, other.id); -+ }, whole: true }); -+ } -+ default: -+ return null; -+ } -+ }), + if (selfId && other.id) actions.openExchange(selfId, other.id); + }, whole: true }); + } +@@ -4105,7 +4105,7 @@ function EarlierParts({ sessionId, roster, actions }) { + return null; + } + }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) + (0, import_react9.createElement)(PartLine, { time: view.startedAt }) ); } --function SettledSecret({ bot, request, roster }) { -- const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( -- "p", -- { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -- )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) -- ) -+function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -+ const roster = useRoster((value) => value); -+ const data = node.data; -+ const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -+ const selfId = roster.byId[sessionId]?.id ?? sessionId; -+ const deferred = source.kind === "bot" && source.deferred === true; -+ const relayed = isRelayContext(data); -+ const turn = node.location?.turn?.turn ?? data.turn; + function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +@@ -4116,7 +4116,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us + const deferred = source.kind === "bot" && source.deferred === true; + const relayed = isRelayContext(data); + const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -+ }, [relayed, sessionId, turn, source.senderSessionId]); -+ const order = useSessions((list) => { -+ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -+ return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -+ }); -+ const peers = useTurnClock((state) => { -+ if (!relayed) return ""; -+ const turns = state[sessionId] ?? {}; -+ return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -+ }); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const hiddenTurns = roster.exchangeTurns[sessionId]; -+ const unreadOpening = useTurnClock((state) => { -+ const first = openingTurn(state[sessionId], order, hiddenTurns); -+ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -+ }); -+ const body = triggerBody(); + if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); + }, [relayed, sessionId, turn, source.senderSessionId]); + const order = useSessions((list) => { +@@ -4135,23 +4135,31 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us + return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; + }); + const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; + return unreadOpening ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -+ function triggerBody() { + function triggerBody() { + if (source.form === "remote-chat") return null; + if (source.shown === true) return null; + if (source.remote?.handle) { + const message = (0, import_react9.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)(FirstContact, { source, roster, sessionId }), message) : message; + } -+ if (source.kind === "bot") { + if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions }); + if (source.role === "room" || source.role === "kickoff") return null; + if (source.role === "mention") return (0, import_react9.createElement)("div", { className: "bt-event" }, (0, import_react9.createElement)("span", null, contentText(data.content))); -+ if (source.role === "secret" || deferred) return null; -+ if (source.role === "post") { -+ if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.role === "secret" || deferred) return null; + if (source.role === "post") { + if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.notice === true || source.senderSessionId === "face-notice") return null; -+ const room2 = roster.roomsById[sessionId]; -+ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + const room2 = roster.roomsById[sessionId]; + const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); + return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -+ } -+ if (source.role === "report") { + } + if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); + return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -+ } + } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); + if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); + if (source.role === "retry" || source.retried === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -+ const run = relayed ? commRunOf( -+ order.split(",").filter(Boolean).map(Number), -+ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -+ turn, -+ hiddenTurns -+ ) : null; -+ if (run === "folded") return null; -+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -+ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -+ const bots = [sender]; -+ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -+ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -+ } + const run = relayed ? commRunOf( + order.split(",").filter(Boolean).map(Number), + Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +@@ -4165,7 +4165,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us + for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { + if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); + } +- return (0, import_react6.createElement)(EventLine, { + return (0, import_react9.createElement)(EventLine, { -+ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -+ bots, -+ onOpen: (bot) => actions.openExchange(selfId, bot.id), -+ whole: true -+ }); -+ } -+ if (source.kind === "schedule") { -+ const prompts = reminderPrompts(contentText(data.content)); + prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), + bots, + onOpen: (bot) => actions.openExchange(selfId, bot.id), +@@ -4174,24 +4174,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us + } + 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")); + return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); -+ } -+ if (source.kind === "user-question-reply") return null; + } + 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")); + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -+ } -+} -+function NewDivider() { + } + } + 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"))); + return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New"))); -+} -+function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -+ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + } + function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { + const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ if (!open) turnProcess.setOpen(true); -+ }, [open, turnProcess]); -+ const turn = node?.location?.turn?.turn ?? node?.data?.turn; -+ const start = node?.location?.turn?.start?.time; -+ const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); + 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)(() => { + (0, import_react9.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; -+ }); + actions.noteTurnTime(sessionId, turn, "first", start); + }, [sessionId, turn, start]); + const visit = useUi((state) => state.visits?.[sessionId]); +@@ -4199,11 +4199,67 @@ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTu + 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; + return fresh ? (0, import_react9.createElement)(NewDivider) : null; -+} -+function Nothing() { -+ return null; -+} + } + function Nothing() { + return null; + } +var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]); +var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name); +var SILENT_REGISTRATION_TOOLS = /* @__PURE__ */ new Set([ @@ -7914,35 +5709,13 @@ diff --git a/client.js b/client.js + if (parts.length === 0) return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · ")); +} -+var SEPARATOR_GAP_MS = 30 * 60 * 1e3; -+function clockLabel(time, now = Date.now()) { -+ const date = new Date(time); -+ const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -+ const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -+ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -+ if (days === 0) return t("Today {time}", { time: clock4 }); -+ if (days === 1) return t("Yesterday {time}", { time: clock4 }); -+ const sameYear = date.getFullYear() === new Date(now).getFullYear(); -+ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; -+} -+function nextTurnStart(turns, turn) { -+ let next; -+ for (const key in turns) { -+ const candidate = Number(key); -+ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; -+ } -+ return next === void 0 ? void 0 : turns[next].first; -+} -+function openingTurn(turns, firstTurns, hiddenTurns) { -+ if (!turns || !firstTurns) return null; -+ const start = nextTurnStart(turns, -Infinity); -+ if (start === void 0) return null; -+ const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -+ const listed = firstTurns.split(",").map(Number); -+ const earlier = listed.filter((entry) => entry < turn); -+ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; -+} -+var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; + var SEPARATOR_GAP_MS = 30 * 60 * 1e3; + function clockLabel(time, now = Date.now()) { + const date = new Date(time); +@@ -4233,10 +4233,19 @@ function openingTurn(turns, firstTurns, hiddenTurns) { + return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; + } + var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +function movedOnSince(visit, turns, hiddenTurns) { + if (typeof visit?.entered !== "number") return false; + for (const key in turns ?? {}) { @@ -7952,204 +5725,105 @@ diff --git a/client.js b/client.js + return false; +} +var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; -+function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -+ const turn = location2.turn; -+ const time = location2.end.time; + function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { + const turn = location2.turn; + const time = location2.end.time; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ actions.noteTurnTime(sessionId, turn, "end", time); -+ }, [sessionId, turn, time]); -+ const label = useTurnClock((state) => { -+ const start = nextTurnStart(state[sessionId] ?? {}, turn); -+ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -+ }); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const fresh = useTurnClock((state) => { -+ if (typeof visit?.seen !== "number" || time > visit.seen) return false; -+ const start = nextTurnStart(state[sessionId] ?? {}, turn); -+ return start !== void 0 && start > visit.seen && start <= visit.entered; -+ }); -+ if (!label && !fresh) return null; + actions.noteTurnTime(sessionId, turn, "end", time); + }, [sessionId, turn, time]); + const label = useTurnClock((state) => { +@@ -4250,15 +4250,15 @@ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions + return start !== void 0 && start > visit.seen && start <= visit.entered; + }); + if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, + return (0, import_react9.createElement)( + import_react9.Fragment, -+ null, + null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null + label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + 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 Shimmer({ children }) { + function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-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"] -+}; -+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) }; - } -- return null; + var PHRASES = { + thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +@@ -4320,19 +4320,22 @@ function toolActivity(name, arg, findBot, self) { } --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"); + function workPhrase(tool) { + if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); - } --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) : ""; + } + function useLiveNote(actions, sessionId, id, entry) { + const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ if (!key) return void 0; -+ actions.noteLive(sessionId, id, JSON.parse(key)); -+ return () => actions.noteLive(sessionId, id, null); -+ }, [sessionId, id, key]); -+} + if (!key) return void 0; + actions.noteLive(sessionId, id, JSON.parse(key)); + return () => actions.noteLive(sessionId, id, null); + }, [sessionId, id, key]); + } +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +function 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]; + if (done) return void 0; + let stop = false; + let timer; +@@ -4341,10 +4341,10 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi + const result = await actions.roomProgress(sessionId); + if (stop) return; + const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; + const key = entry ? JSON.stringify(entry.replies) : ""; -+ if (entry && key !== seen) { -+ seen = key; + if (entry && key !== seen) { + seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); + 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 = []; -+ } -+ } + } + timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); + }; +@@ -4362,71 +4362,89 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi + replies = []; + } + } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; + if (replies.length === 0) return null; -+ const room2 = roster.roomsById[sessionId]; + const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( + 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)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) -- ) -+ { className: "bt-group-replies" }, -+ replies.map((reply, index) => { + { className: "bt-group-replies" }, + replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; + const stored = roster.byId[reply.botId]; + const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored; + const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -+ if (reply.kind === "user") { + if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( + const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId; + if (reply.other) { + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( @@ -8159,45 +5833,67 @@ diff --git a/client.js b/client.js + ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); -+ } -+ if (reply.kind === "failed") { + } + if (reply.kind === "failed") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", + "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, + { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status" }, -+ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -+ ); -+ } -+ if (reply.kind === "lookup") { + t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) + ); + } + if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { key: `${reply.botId}-${index}`, className: "bt-event" }, -+ reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -+ ); -+ } -+ if (reply.kind === "missed") { + "div", + { key: `${reply.botId}-${index}`, className: "bt-event" }, + reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) + ); + } + if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -+ } -+ if (reply.kind === "resumed") { + } + if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -+ } -+ if (reply.kind === "cut") { + } + if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -+ } -+ if (reply.kind === "approval" || reply.kind === "working") { + } + if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { key: `${reply.botId}-${index}`, className: "bt-event" }, + "div", + { key: `${reply.botId}-${index}`, className: "bt-event" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, + "button", + { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -+ ) -+ ); -+ } -+ if (reply.kind === "stopped") { + ) + ); + } + if (reply.kind === "stopped") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); -+ } + } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); +- }), +- pending.map((id) => { +- const bot = roster.byId[id]; +- if (!bot) return null; +- return (0, import_react6.createElement)( +- "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); + if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null; + if ((reply.kind === "notice" || reply.kind === "left") && leaveNoticeCoveredByClose(room2, reply.text)) return null; + if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") { @@ -8217,224 +5913,154 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react9.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); -+ }) + }) ); } --function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!long) return; -- void Promise.resolve(onSave(value)).then((ok) => { -- if (ok) { -- setValue(""); -- setReplaced(true); -+var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); -+var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); -+function callTree(root) { -+ const calls = []; -+ const seen = /* @__PURE__ */ new Set(); +@@ -4435,52 +4435,52 @@ var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); + function callTree(root) { + const calls = []; + const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; + const visit = (call4) => { + const id = call4.callId ?? call4; -+ if (seen.has(id)) return; -+ seen.add(id); + if (seen.has(id)) return; + seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); -+ }; -+ visit(root); -+ return calls; -+} -+function useShownCards(actions, sessionId, callIds) { -+ const key = JSON.stringify(callIds); + }; + visit(root); + return calls; + } + function useShownCards(actions, sessionId, callIds) { + const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ const ids = JSON.parse(key); -+ if (ids.length === 0 || !actions.noteCards) return void 0; -+ actions.noteCards(sessionId, ids, 1); -+ return () => actions.noteCards(sessionId, ids, -1); -+ }, [sessionId, key]); -+} + const ids = JSON.parse(key); + if (ids.length === 0 || !actions.noteCards) return void 0; + actions.noteCards(sessionId, ids, 1); + return () => actions.noteCards(sessionId, ids, -1); + }, [sessionId, key]); + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; + const { text: arg, value: argValue } = toolArgs(call4); -+ const self = roster.byId[sessionId]; -+ switch (tool) { -+ case "message_bot": { -+ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; + const self = roster.byId[sessionId]; + switch (tool) { + case "message_bot": { + const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -+ } -+ case "stop_bot": { -+ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + } + case "stop_bot": { + const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -+ } -+ case "create_bot": { -+ const created = findBot(arg("name")); -+ if (!done || created === void 0) return null; + } + case "create_bot": { + const created = findBot(arg("name")); + if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); + return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -+ } -+ case "update_bot": { -+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); + } + case "update_bot": { + const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; + return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -+ } -+ case "group_relay": + } + case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); + return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi }); -+ case "create_group": -+ case "update_group": -+ case "delete_group": -+ case "post_to_group": { -+ const verbs = { -+ create_group: ["Created group", "Couldn't create the group"], -+ update_group: ["Updated group", "Couldn't update the group"], -+ delete_group: ["Deleted group", "Couldn't delete the group"], -+ post_to_group: ["Posted in", "Couldn't post in the group"] -+ }[tool]; -+ if (!done) return null; + case "create_group": + case "update_group": + case "delete_group": +@@ -4492,64 +4492,90 @@ function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot + post_to_group: ["Posted in", "Couldn't post in the group"] + }[tool]; + if (!done) return null; +- const result = contentText(call2.content); + const result = contentText(call4.content); -+ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); + const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); + if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -+ const name = named[2]; -+ const room2 = roster.rooms.find((entry) => entry.name === name); + const name = named[2]; + const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); + return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -+ } -+ case "ask_user": { -+ const question = roster.questions?.[sessionId]; + } + case "ask_user": { + const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); + if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true }); + return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -+ } -+ case "request_secret": + } + case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); + return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions }); -+ case "ask_user_question": { -+ if (!done) return null; -+ let parsed = null; -+ try { + case "ask_user_question": { + if (!done) return null; + let parsed = null; + try { +- parsed = JSON.parse(contentText(call2.content)); + parsed = JSON.parse(contentText(call4.content)); -+ } catch { -+ parsed = null; + } catch { + parsed = null; } -- }); -- }; -- const edit = (next) => { -- setValue(next); -- setReplaced(false); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -- ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null -- ); -+ const answer = parsed?.answers?.[0]; -+ const question = argValue("questions")?.[0]; -+ if (!answer) return null; + const answer = parsed?.answers?.[0]; + const question = argValue("questions")?.[0]; + if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); + return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); -+ } -+ case "read_group_chat": { -+ if (!done) return null; -+ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); + } + case "read_group_chat": { + if (!done) return null; + const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; + return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -+ } -+ case "remember": -+ case "forget": + } + 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; + return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null; -+ default: -+ return null; -+ } + default: + return null; + } } --function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { -- if (!open) setConfirm(false); -- }, [open]); -- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) -- ), -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-control" }, -- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -- ) -- ), -- open ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit" }, -- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), -- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- confirm ? [ -- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), -- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), -- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) -- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) -- ) -- ) : null -- ); +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { -+ const roster = useRoster((value) => value); -+ const root = node.data.root; -+ const turn = node.location?.turn?.turn ?? root.turn; -+ const hidden = isHiddenTurn(roster, sessionId, turn); + const roster = useRoster((value) => value); + const root = node.data.root; + const turn = node.location?.turn?.turn ?? root.turn; + const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); + const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId); -+ const calls = hidden ? [] : callTree(root); + const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); + const drawn = calls.filter((call4) => DRAWN.has(toolName(call4))); + const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result"); -+ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -+ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); + const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; + useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); + useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId)); -+ if (drawn.length === 0) return null; + if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); - } --function AddSecret({ actions, run, onDone }) { -- const [name, setName] = (0, import_react5.useState)(""); -- const [purpose, setPurpose] = (0, import_react5.useState)(""); -- const [value, setValue] = (0, import_react5.useState)(""); -- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!ready) return; -- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -- if (ok) { -- setValue(""); -- onDone(); -- } -- }); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) -- ) -- ); ++} + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -8442,30 +6068,7 @@ diff --git a/client.js b/client.js + const at = roster?.remoteAwaiting?.[sessionId]; + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; - } --function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); -- const secrets = roster.secrets ?? []; -- return [ -- (0, import_react5.createElement)( -- "p", -- { key: "about", className: "bt-secret-about" }, -- t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), -- t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), -- t(" before anything goes to a model.") -- ), -- (0, import_react5.createElement)( -- Section, -- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -- ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -- ]; ++} + +// src/client/remote-approval-wait.js +var REMOTE_APPROVAL_LABEL = "Waiting for their approval"; @@ -8473,76 +6076,8 @@ diff --git a/client.js b/client.js + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; - } --var SECRET_CSS = ` --.bt-secret-head{align-items:center;gap:10px} --.bt-secret-who{position:relative;flex:none;display:inline-flex} --.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} --.bt-secret-lock svg{width:11px;height:11px} --.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} --.bt-secret-body{display:flex;flex-direction:column;gap:8px} --.bt-secret-field{align-items:center;min-height:36px} --.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} --.bt-secret-input[data-masked]{-webkit-text-security:disc} --.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} --.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} --.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-secret-glide[data-at="1"]{transform:translateX(100%)} --.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} --.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} --.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --/* A key card asks the user to act, so the process group carrying it stays open in every -- work-details mode (same override as .bt-group-replies in styles.js). */ --[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} --[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} --.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} --.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} --@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} --.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} --.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} --.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} --.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} --.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} --.bt-secret-done .bt-q-head{align-items:center} --.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} --.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} --.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} --@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} --@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} --.bt-secret-event{display:inline-flex;align-items:center;gap:5px} --.bt-secret-dock{flex-direction:column;align-items:center} --.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} --.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} --.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} --.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} --.bt-secret-faces{display:inline-flex} --.bt-secret-faces>*+*{margin-left:-6px} --.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} --.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} --.bt-secret-radios{display:flex;gap:16px;font-size:13px} --.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} --.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} --.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} --.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} --.bt-secret-bot svg{width:13px;height:13px} --.bt-secret-inline{display:flex;gap:8px;align-items:center} --.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} --.bt-secret-row-actions .bt-set-hint{flex:1} --.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} --.bt-secret-danger svg{width:13px;height:13px} --.bt-secret-add .bt-input{height:34px} --@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} --`; - --// src/client/turn-reasoning.js --function turnReasoning(steps, current) { -- const members = steps.length ? steps : [current]; -- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); ++} ++ +// src/client/activity-row-host.js +var RUNNING_HOST = "[data-chat-flow] [data-chat-running]"; +var TAIL_HOST = "tail"; @@ -8552,67 +6087,20 @@ diff --git a/client.js b/client.js + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); } --var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); --var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); --var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); --// src/client/cells.js --var import_react6 = require("react"); --var import_react_dom = require("react-dom"); --var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); --var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); --function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { -- const onClick = (event) => { -- const target = mentionTarget(event); -- if (target === null) return; -- event.preventDefault(); -- event.stopPropagation(); -- actions.openSession(target); -- }; -- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -- texts2.map((text, index) => { -- const key = msgKey ? `${msgKey}:${index}` : void 0; -- const picked = key ? reactions?.[index] ?? [] : []; -- const last = index === texts2.length - 1; -- const live = streaming && last; -- return (0, import_react6.createElement)( -- "div", -- { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( -- "div", -- { className: "bt-msg-line" }, -- lead && last ? lead : null, -- (0, import_react6.createElement)( -- "div", -- { className: "bt-bubble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) -- ), -- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) -- ), -- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { -- key: emoji, -- type: "button", -- className: "bt-react", -- "aria-label": t("Remove reaction {emoji}", { emoji }), -- onClick: () => actions.toggleReaction(msgKey, index, emoji) -- }, emoji))) : null -- ); -- }) -- ); -+// src/client/activity-row.js + // src/client/activity-row.js +-var import_react7 = require("react"); +var import_react10 = require("react"); -+var import_react_dom2 = require("react-dom"); -+var PHRASE_HOLD_MS = 800; -+var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); -+function elapsedLabel(ms) { -+ const minutes = Math.floor(ms / 6e4); -+ if (minutes < 60) return `${Math.max(1, minutes)}m`; -+ const hours = Math.floor(minutes / 60); -+ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; -+} + var import_react_dom2 = require("react-dom"); + var PHRASE_HOLD_MS = 800; + var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +@@ -4559,9 +4559,69 @@ function elapsedLabel(ms) { + const hours = Math.floor(minutes / 60); + return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -8664,126 +6152,57 @@ diff --git a/client.js b/client.js + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); + }); -+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -+ check(); -+ return () => { -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ }; -+ }, [remote, sending]); -+ return replied; -+} -+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; -+ } -+ let frame = 0; -+ const find = () => { -+ frame = 0; -+ const node = findHost(where); -+ setHost((prev) => prev === node ? prev : node); -+ }; -+ const observer = new MutationObserver(() => { -+ if (!frame) frame = requestAnimationFrame(find); -+ }); -+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -+ find(); -+ return () => { -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ }; -+ }, [active, where]); -+ return host; - } --var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; --function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (refs.some((ref) => ref.current?.contains(event.target))) return; -- close(); -+function useRowEntrances(replacing) { -+ const check = (0, import_react10.useRef)(replacing); -+ check.current = replacing; -+ (0, import_react10.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 onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -+ const scan = () => { -+ frame = 0; -+ const next = document.querySelector("[data-chat-flow]"); -+ if (next !== column) { -+ observer.disconnect(); -+ column = next; -+ if (column) observer.observe(column, { childList: true, subtree: true }); -+ } -+ if (!column) return; -+ const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -+ let last = -1; -+ items.forEach((item, index) => { -+ if (seen.has(item)) last = index; -+ }); -+ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -+ for (const item of fresh) seen.add(item); -+ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -+ for (const item of fresh) { -+ if (items.indexOf(item) < last) continue; -+ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -+ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -+ item.addEventListener("animationend", settle); - } - }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -+ const observer = new MutationObserver(() => { -+ if (!frame) frame = requestAnimationFrame(scan); -+ }); -+ scan(); - return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); ++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); ++ check(); ++ return () => { + cancelAnimationFrame(frame); + observer.disconnect(); ++ }; ++ }, [remote, sending]); ++ return replied; ++} ++function useRunningHost(active, where = RUNNING_HOST) { ++ const [host, setHost] = (0, import_react10.useState)(null); ++ (0, import_react10.useEffect)(() => { + if (!active) { + setHost(null); + return void 0; +@@ -4569,7 +4569,7 @@ function useRunningHost(active) { + let frame = 0; + const find = () => { + frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); ++ const node = findHost(where); + setHost((prev) => prev === node ? prev : node); }; -- }, [open]); -+ }, []); + const observer = new MutationObserver(() => { +@@ -4581,13 +4581,13 @@ function useRunningHost(active) { + cancelAnimationFrame(frame); + observer.disconnect(); + }; +- }, [active]); ++ }, [active, where]); + return host; } --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); -- const close = () => setPanel(null); -- useDismiss(panel !== null, close, [tools, pop]); -- const open = (kind, event) => { -- if (panel === kind) { -- close(); -- return; -+function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -+ const roster = useRoster((value) => value); -+ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -+ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -+ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -+ const bot = roster.byId[sessionId]; -+ const room2 = roster.roomsById[sessionId]; + function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); ++ const check = (0, import_react10.useRef)(replacing); + check.current = replacing; +- (0, import_react7.useEffect)(() => { ++ (0, import_react10.useEffect)(() => { + const seen = /* @__PURE__ */ new WeakSet(); + const started = Date.now(); + let column = null; +@@ -4638,21 +4638,31 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us + 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 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 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 awaiting = useAwaitingReply(roster, sessionId); + const sendRun = (0, import_react10.useRef)(false); + if (!running) sendRun.current = false; @@ -8794,59 +6213,49 @@ diff --git a/client.js b/client.js + const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running; + lastRun.current = { running, send: sendRun.current }; + const steady = bridging; -+ const tool = tools.at(-1); + const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); + const inputRef = (0, import_react10.useRef)(0); -+ const inputAt = Math.max(inputRef.current, pendingInput); -+ inputRef.current = inputAt; + 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 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 wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); + const markState = (0, import_react10.useRef)("thinking"); -+ if (want) markState.current = want.state; + if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { + (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 rect = event.currentTarget.getBoundingClientRect(); -- const below = rect.bottom + 220 < window.innerHeight; -- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -- setPanel(kind); -- }; -- const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { -- setCopied(true); -- setTimeout(() => setCopied(false), 1e3); -- }); -- }; -- const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -- link.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); -- }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -- close(); -- run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( -+ const timer = setTimeout(make, wait); -+ return () => clearTimeout(timer); -+ }, [wantKey, shown]); + if (wantKey === null || shown?.key === wantKey) return void 0; + const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); + const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; +@@ -4663,26 +4663,31 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us + 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); + const [phase, setPhase] = (0, import_react10.useState)(null); + const endedAt2 = (0, import_react10.useRef)(0); + const shift = (0, import_react10.useRef)(null); + const leaving = (0, import_react10.useRef)(false); -+ leaving.current = phase !== null && !visible; + leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { + (0, import_react10.useEffect)(() => { -+ if (visible) { -+ setPhase("in"); -+ return void 0; -+ } + 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); + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -8857,29 +6266,35 @@ diff --git a/client.js b/client.js + }; + if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS); + else leave(); -+ return () => clearTimeout(timer); -+ }, [visible]); + return () => clearTimeout(timer); + }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { + (0, import_react10.useLayoutEffect)(() => { -+ if (phase !== null) return; + if (phase !== null) return; +- endedAt.current = Date.now(); + endedAt2.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]); + setShown(null); + const before = shift.current; + shift.current = null; +@@ -4691,31 +4691,37 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us + 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)(() => { + (0, import_react10.useEffect)(() => { -+ if (!running) actions.clearLive(sessionId); -+ }, [running, sessionId]); + 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)(() => { + useRowEntrances(() => leaving.current || Date.now() - endedAt2.current < 300); + const [now, setNow] = (0, import_react10.useState)(Date.now); + (0, import_react10.useEffect)(() => { -+ if (phase !== "in") return void 0; -+ const timer = setInterval(() => setNow(Date.now()), 1e4); -+ return () => clearInterval(timer); -+ }, [phase]); + if (phase !== "in") return void 0; + const timer = setInterval(() => setNow(Date.now()), 1e4); + return () => clearInterval(timer); + }, [phase]); +- const host = useRunningHost(running); + 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); @@ -8887,70 +6302,36 @@ diff --git a/client.js b/client.js + (0, import_react10.useEffect)(() => { + drawn.current = Boolean(host && phase !== null && shown); + }, [host, phase, shown]); -+ if (!host || phase === null || !shown) return null; -+ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + 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)( + return (0, import_react_dom2.createPortal)((0, import_react10.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 -- ); +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( + { className: "bt-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" }, + "span", + { className: "bt-typing-text" }, +- (0, import_react7.createElement)( + (0, import_react10.createElement)( -+ "span", -+ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + "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 + (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null -+ ) -+ ) -+ ), host); - } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( -- "details", -- { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) -- ); -+ -+// src/client/dsh-offer.js -+var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); -+var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; -+var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); -+function showsDshRow(dsh, app) { -+ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -+ if (dsh.kind === "npm") return true; -+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); + ) + ) + ), host); +@@ -4731,49 +4731,1896 @@ function showsDshRow(dsh, app) { + return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } --function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -- const tail = useTurnData?.("turn-tail"); -- const location2 = node.location; -- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -- const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { -- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -- }, [fileMentions, openFile, seq, tail, turn]); -+ + +-// src/client/sidebar.js +-var import_react8 = require("react"); +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; @@ -8968,41 +6349,7 @@ diff --git a/client.js b/client.js + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); + }; + }, []); - } --function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const steps = useTurnSteps(String(data.turn)); -- const contexts = useTurnContext(String(data.turn)); -- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); -- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); -- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); -- const running = data.status === "running"; -- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); -- if (hidden) return null; -- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; -- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- relays.map((relay) => { -- const source = relay.source; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { -- key: relay.seq, -- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -- bots: [sender], -- onOpen: () => { -- if (sender.id) actions.openExchange(selfId, sender.id); -- }, -- whole: true -- }); -- }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null -- ); ++} +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -9033,31 +6380,7 @@ diff --git a/client.js b/client.js + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; + }); - } --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) -- ) -- ); -- } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); ++} +function 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] })); @@ -9072,11 +6395,8 @@ diff --git a/client.js b/client.js + 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 }] : []; - }); -- 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)( ++ }); ++} +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -9098,18 +6418,12 @@ diff --git a/client.js b/client.js + const text2 = `${label} · ${workText(tool, step)}`; + const elapsed = elapsedSince(now, at); + return (0, import_react12.createElement)( - "button", -- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) ++ "button", + { type: "button", className: "bt-mw-row", "aria-label": [text2, 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, text2)), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null - ); -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-group-msg" }, -- (0, import_react6.createElement)( ++ ); +} +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); @@ -9119,26 +6433,15 @@ diff --git a/client.js b/client.js + { type: "button", className: "bt-mw-row bt-mw-line", "aria-label": [waiting, doing, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, + (0, import_react12.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react12.createElement)(Face, { key: entry.id, entry, size: 16 }))), + (0, import_react12.createElement)( - "span", -- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, -- bot.name, -- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null ++ "span", + { className: "bt-mw-row-text" }, + waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, + waiting && doing ? " · " : null, + doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null - ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ++ ), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null - ); - } --function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("span", null, prefix), -- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null -- ); ++ ); ++} +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -9172,16 +6475,7 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); - } --function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -- const self = roster.byId[sessionId]; -- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -- const other = team || !scope ? void 0 : findBot(scope); -- const target = other && other.id !== self?.id ? other : self; -- if (!target) return null; -- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); -- return (0, import_react6.createElement)( ++} +function useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -9220,9 +6514,7 @@ diff --git a/client.js b/client.js + void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); + }; + return (0, import_react12.createElement)( - "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) ++ "div", + { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react12.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react12.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, @@ -9239,14 +6531,8 @@ diff --git a/client.js b/client.js + { className: "bt-mw-actions" }, + (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 PartLine({ time }) { -- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); --} --var scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; ++ ); ++} +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -9256,99 +6542,17 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), + entry.approvals.map((approval) => (0, import_react12.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); - } -- return null; --}; --function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); -- const load = (cursor) => { -- if (busy.current) return; -- busy.current = true; -- setView((current) => ({ ...current, loading: true, failed: false })); -- actions.earlier(sessionId, cursor).then((page) => { -- busy.current = false; -- if (!alive.current) return; -- const scroller = top.current && scrollParent(top.current); -- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -- }, () => { -- busy.current = false; -- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); -- }); ++ } + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); + void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); - }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); -- return () => { -- alive.current = false; -- }; -- }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { -- const pending = restore.current; -- restore.current = null; -- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -- }, [view.items]); -- (0, import_react6.useEffect)(() => { -- const node = top.current; -- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -- const observer = new IntersectionObserver((entries2) => { -- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -- observer.observe(node); -- return () => observer.disconnect(); -- }, [view.cursor, view.loading, view.failed]); -- const selfId = roster.byId[sessionId]?.id; -- const rows = []; -- for (const item of view.items) { -- const last = rows.at(-1); -- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -- } -- return (0, import_react6.createElement)( ++ }; + return (0, import_react12.createElement)( - "div", -- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( ++ "div", + { className: "bt-mw-member" }, + (0, import_react12.createElement)( - "div", -- { ref: top, className: "bt-earlier-top" }, -- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null -- ), -- rows.map((row, index) => { -- const key = rows.length - index; -- switch (row.kind) { -- case "divider": -- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); -- case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, -- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -- )); -- case "answer": -- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); -- case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -- case "event": { -- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); -- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -- if (selfId && other.id) actions.openExchange(selfId, other.id); -- }, whole: true }); -- } -- default: -- return null; ++ "div", + { className: "bt-mw-state bt-mw-work" }, + (0, import_react12.createElement)("span", null, (0, import_react12.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null @@ -9533,39 +6737,11 @@ diff --git a/client.js b/client.js + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; - } -- }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) -- ); ++ } + }; + }, []); + return workflows; - } --function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- const deferred = source.kind === "bot" && source.deferred === true; -- const relayed = isRelayContext(data); -- const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { -- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -- }, [relayed, sessionId, turn, source.senderSessionId]); -- const order = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -- }); -- const peers = useTurnClock((state) => { -- if (!relayed) return ""; -- const turns = state[sessionId] ?? {}; -- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const hiddenTurns = roster.exchangeTurns[sessionId]; -- const unreadOpening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], order, hiddenTurns); -- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++} +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -9668,44 +6844,7 @@ diff --git a/client.js b/client.js + title: count === 1 ? t("1 message waits for your approval") : t("{count} messages wait for your approval", { count }), + hint: t("Approve it in the chat"), + ...extra - }); -- const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -- function triggerBody() { -- if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; -- if (source.role === "secret" || deferred) return null; -- if (source.role === "post") { -- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; -- const room2 = roster.roomsById[sessionId]; -- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -- } -- if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -- } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -- const run = relayed ? commRunOf( -- order.split(",").filter(Boolean).map(Number), -- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -- turn, -- hiddenTurns -- ) : null; -- if (run === "folded") return null; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -- const bots = [sender]; -- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -- } -- return (0, import_react6.createElement)(EventLine, { -- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -- bots, -- onOpen: (bot) => actions.openExchange(selfId, bot.id), -- whole: true -- }); ++ }); + for (const bot of roster.bots ?? []) { + const count = bot.pendingCount ?? 0; + if (count <= 0) continue; @@ -9784,10 +6923,7 @@ diff --git a/client.js b/client.js + const column = columnOf(task); + if (column === "in-progress") progress.push({ ...card, status: "task", since: startedAtOf(task) }); + else if (column === "done" && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "cancelled" ? "cancelled" : "done" }); - } -- 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")); ++ } + } + for (const graph of Array.isArray(workflows) ? workflows : []) { + if (graph === null || typeof graph !== "object") continue; @@ -9799,9 +6935,7 @@ diff --git a/client.js b/client.js + const at2 = endedAt(run); + if (!fresh(at2)) continue; + if (latest === void 0 || at2 > endedAt(latest)) latest = run; - } -- if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); ++ } + if (latest === void 0) continue; + const at = endedAt(latest); + const name = typeof graph.name === "string" && graph.name.trim() !== "" ? graph.name.trim() : String(graph.id ?? t("A workflow")); @@ -9854,7 +6988,7 @@ diff --git a/client.js b/client.js + const custom = typeof reply.custom === "string" ? reply.custom.trim() : ""; + if (selected.length === 0 && custom === "") return void 0; + return Promise.resolve(actions.answerQuestion(card.sessionId, card.question.id, { selected, custom })); - } ++ } + if (card?.kind === "task" && card.status === "input") { + const text2 = typeof choice === "string" ? choice.trim() : ""; + const questionId = card.task?.inputRequest?.id; @@ -9871,9 +7005,7 @@ diff --git a/client.js b/client.js + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 0; - } --function NewDivider() { -- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); ++} +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -9927,24 +7059,7 @@ diff --git a/client.js b/client.js + } }, t("Send back")), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "accept") }, t("Accept")) + ); - } --function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { -- if (!open) turnProcess.setOpen(true); -- }, [open, turnProcess]); -- const turn = node?.location?.turn?.turn ?? node?.data?.turn; -- const start = node?.location?.turn?.start?.time; -- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "first", start); -- }, [sessionId, turn, start]); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- const own = state[sessionId]?.[turn]; -- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; -- }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; ++} +function AnswerFields({ card, busy, onAnswer }) { + const [, redraw] = (0, import_react13.useState)(0); + const question = card.kind === "question"; @@ -10004,9 +7119,7 @@ diff --git a/client.js b/client.js + }), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); - } --function Nothing() { -- return null; ++} +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { + const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; @@ -10030,31 +7143,15 @@ diff --git a/client.js b/client.js + wide ? (0, import_react13.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null + ); - } --var SEPARATOR_GAP_MS = 30 * 60 * 1e3; --function clockLabel(time, now = Date.now()) { -- const date = new Date(time); -- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -- if (days === 0) return t("Today {time}", { time: clock4 }); -- if (days === 1) return t("Yesterday {time}", { time: clock4 }); -- const sameYear = date.getFullYear() === new Date(now).getFullYear(); -- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; ++} +function Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; + return (0, import_react13.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", null, t(label))); - } --function nextTurnStart(turns, turn) { -- let next; -- for (const key in turns) { -- const candidate = Number(key); -- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; ++} +function whenText(card, now) { + if (card.status === "running" || card.status === "task") { + return typeof card.since === "number" && now - card.since >= 6e4 ? t("for {time}", { time: elapsedLabel(now - card.since) }) : t("just started"); - } -- return next === void 0 ? void 0 : turns[next].first; ++ } + return typeof card.at === "number" ? relativeShort(card.at) : ""; +} +function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) { @@ -10116,15 +7213,7 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) + ); - } --function openingTurn(turns, firstTurns, hiddenTurns) { -- if (!turns || !firstTurns) return null; -- const start = nextTurnStart(turns, -Infinity); -- if (start === void 0) return null; -- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -- const listed = firstTurns.split(",").map(Number); -- const earlier = listed.filter((entry) => entry < turn); -- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; ++} +function useNow2(ms) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -10132,30 +7221,7 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [ms]); + return now; - } --var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; --function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -- const turn = location2.turn; -- const time = location2.end.time; -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "end", time); -- }, [sessionId, turn, time]); -- const label = useTurnClock((state) => { -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- if (typeof visit?.seen !== "number" || time > visit.seen) return false; -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start > visit.seen && start <= visit.entered; -- }); -- if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++} +function openCard(actions, card) { + actions.closeInbox(); + if (card?.kind === "workflow") { @@ -10239,10 +7305,8 @@ diff --git a/client.js b/client.js + )) + ) + ) - ); - } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); ++ ); ++} +var INBOX_CSS = ` +.bt-inbox-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-inbox-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -10307,10 +7371,7 @@ diff --git a/client.js b/client.js + id: String(task.id), + title: String(task.title ?? "") + }; - } --var PHRASES = { -- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] ++} + +// src/client/board-card.js +var import_react14 = require("react"); @@ -10324,42 +7385,7 @@ diff --git a/client.js b/client.js + label: "bt-board-label", + owner: "bt-board-who", + note: "bt-board-note" - }; --var TOOL_PHRASES = [ -- [/web_?search|search_?web/i, "Searching online"], -- [/fetch|browse|browser|web/i, "Reading a page"], -- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], -- [/^(read|view|cat)\b|read_/i, "Opening a file"], -- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], -- [/write|edit|patch|apply|replace/i, "Writing changes"], -- [/image|photo|draw/i, "Working on an image"] --]; --var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; --function toolActivity(name, arg, findBot, self) { -- switch (name) { -- case "ask_user": -- return null; -- case "request_secret": -- return null; -- case "list_secrets": -- return { label: t("Checking the keys"), state: "searching" }; -- case "ask_user_question": -- return { label: t("Waiting for your answer"), state: "alert" }; -- case "group_relay": -- return { relay: true, state: "orbit" }; -- case "message_bot": { -- const target = findBot(arg("to")); -- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -- } -- case "stop_bot": { -- const target = findBot(arg("bot")); -- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; -- } -- case "create_bot": -- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; ++}; +function BoardCard({ + card, + skin = TASK_SKIN, @@ -10487,19 +7513,7 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); - } -- case "list_bots": -- return { label: t("Checking the team"), state: "searching" }; -- case "read_group_chat": -- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; -- case "read_own_chat": -- return { label: t("Looking back through the chat"), state: "searching" }; -- case "remember": -- return { label: t("Saving to memory"), state: "working" }; -- case "forget": -- return { label: t("Updating memory"), state: "working" }; -- case "recall": -- return { label: t("Checking memory"), state: "searching" }; ++ } + }; + 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 })); @@ -10533,42 +7547,10 @@ diff --git a/client.js b/client.js + return t("Cancelled"); + case "failed": + return t("Failed"); - default: -- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; -- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; -- return { label: phrase ? t(phrase) : null, state: toolState(name) }; ++ default: + return t("Not started"); - } - } --function workPhrase(tool) { -- if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); --} --function useLiveNote(actions, sessionId, id, entry) { -- const key = entry ? JSON.stringify(entry) : ""; -- (0, import_react6.useEffect)(() => { -- if (!key) return void 0; -- actions.noteLive(sessionId, id, JSON.parse(key)); -- return () => actions.noteLive(sessionId, id, null); -- }, [sessionId, id, key]); --} --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { -- if (done) return void 0; -- let stop = false; -- let timer; -- let seen = ""; -- const tick = async () => { -- const result = await actions.roomProgress(sessionId); -- if (stop) return; -- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; -- if (entry && key !== seen) { -- seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); -- } -- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); ++ } ++} +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -10617,19 +7599,13 @@ diff --git a/client.js b/client.js + if (stopped || !Array.isArray(list)) return; + setArchive(list.filter((task) => columnOf(task) === "done")); + }).catch(() => void 0); - }; -- void tick(); ++ }; + load(); + const timer = setInterval(load, POLL_MS3); - return () => { -- stop = true; -- clearTimeout(timer); ++ return () => { + stopped = true; + clearInterval(timer); - }; -- }, [done, sessionId, callId]); -- let replies = live.replies; -- if (done) { ++ }; + }, [key]); + const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + const replace = (task) => { @@ -10872,20 +7848,11 @@ diff --git a/client.js b/client.js + const task = tasks.find((item) => item.id === id) ?? own.find((item) => item.id === id); + if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return; + setError(""); - try { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; ++ try { + replace(await actions.taskUpdate({ id, column })); + } catch (failure) { + setError(failure?.message ?? String(failure)); - } -- } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; -- const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ } + }; + const create = async (title) => { + const text2 = String(title ?? "").trim(); @@ -10900,73 +7867,14 @@ diff --git a/client.js b/client.js + }; + const name = scopeName(roster, scope); + return (0, import_react15.createElement)( - "div", -- { className: "bt-group-replies" }, -- replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -- if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); -- } -- if (reply.kind === "failed") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, -- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -- ); -- } -- if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -- ); -- } -- if (reply.kind === "missed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -- } -- if (reply.kind === "resumed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -- } -- if (reply.kind === "cut") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -- } -- if (reply.kind === "approval" || reply.kind === "working") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -- ) -- ); -- } -- if (reply.kind === "stopped") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); -- } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); -- }), -- pending.map((id) => { -- const bot = roster.byId[id]; -- if (!bot) return null; -- return (0, import_react6.createElement)( ++ "div", + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react15.createElement)( + "div", + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react15.createElement)( - "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); -- }) ++ "div", + { className: "bt-mem-head" }, + (0, import_react15.createElement)("h2", { id: "bt-board-heading" }, t("Board")), + (0, import_react15.createElement)("span", { className: "bt-mem-status" }, name), @@ -10991,22 +7899,8 @@ diff --git a/client.js b/client.js + onCreate: create + }) + ) - ); - } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; -- const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); -- }; -- visit(root); -- return calls; ++ ); ++} +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)} @@ -11068,15 +7962,7 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} - } --function useShownCards(actions, sessionId, callIds) { -- const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { -- const ids = JSON.parse(key); -- if (ids.length === 0 || !actions.noteCards) return void 0; -- actions.noteCards(sessionId, ids, 1); -- return () => actions.noteCards(sessionId, ids, -1); -- }, [sessionId, key]); ++} +`; + +// src/client/role-labels.js @@ -11084,86 +7970,11 @@ diff --git a/client.js b/client.js +function roleText(role) { + if (role === null || role === void 0 || role === "") return ""; + return t(ROLE_LABELS[role] ?? String(role)); - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -- } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -- } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; -- } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); -- } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -- } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; ++} + +// src/client/projects-nav.js +var import_react16 = require("react"); -+var import_react_dom3 = require("react-dom"); + var import_react_dom3 = require("react-dom"); +var PROJECTS_NAV_CSS = ` +.bt-projects-row { display: flex; align-items: center; gap: 8px; } +.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; } @@ -11196,40 +8007,14 @@ diff --git a/client.js b/client.js + for (const role of choices) { + if (role === current) continue; + items.push({ role, label: roleText(role), current: false, pick: true }); - } ++ } + return items; - } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { -- const roster = useRoster((value) => value); -- const root = node.data.root; -- const turn = node.location?.turn?.turn ?? root.turn; -- const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); -- const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); -- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); -- if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); ++} +function roleNote(answer) { + if (answer?.changed === true) return t("Role changed."); + const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused"); + return t("Role unchanged: {reason}", { reason }); - } -- --// src/client/activity-row.js --var import_react7 = require("react"); --var import_react_dom2 = require("react-dom"); --var PHRASE_HOLD_MS = 800; --var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); --function elapsedLabel(ms) { -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${Math.max(1, minutes)}m`; -- const hours = Math.floor(minutes / 60); -- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++} +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)( + "button", @@ -11255,50 +8040,14 @@ diff --git a/client.js b/client.js + rows, + note === "" || note === void 0 ? null : (0, import_react16.createElement)("div", { className: "bt-projects-note", role: "status" }, note) + ); - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { -- if (!active) { -- setHost(null); -- return void 0; -- } -- let frame = 0; -- const find = () => { -- frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -- setHost((prev) => prev === node ? prev : node); -- }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(find); -- }); -- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -- find(); -- return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- }; -- }, [active]); -- return host; ++} +function Chevron() { + return (0, import_react16.createElement)( + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react16.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }) + ); - } --function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); -- check.current = replacing; -- (0, import_react7.useEffect)(() => { -- const seen = /* @__PURE__ */ new WeakSet(); -- const started = Date.now(); -- let column = null; -- let frame = 0; -- const settle = (event) => { -- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -- delete event.currentTarget.dataset.btEnter; -- event.currentTarget.removeEventListener("animationend", settle); ++} +function ProjectsMark() { + return (0, import_react16.createElement)( + "svg", @@ -11312,75 +8061,16 @@ diff --git a/client.js b/client.js + (0, import_react16.useEffect)(() => { + const onDown = (event) => { + if (!ref.current?.contains(event.target)) onClose(); - }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; -- }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); -- } ++ }; + const onKey = (event) => { + if (event.key === "Escape") onClose(); - }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(scan); -- }); -- scan(); ++ }; + window.addEventListener("mousedown", onDown, true); + window.addEventListener("keydown", onKey, true); - return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); ++ return () => { + window.removeEventListener("mousedown", onDown, true); + window.removeEventListener("keydown", onKey, true); - }; -- }, []); --} --function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -- const roster = useRoster((value) => value); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -- const streaming = entries2.some((entry) => entry.text); -- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -- const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); -- const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); -- const inputAt = Math.max(inputRef.current, pendingInput); -- inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); -- const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); -- if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { -- if (wantKey === null || shown?.key === wantKey) return void 0; -- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); -- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -- if (wait <= 0) { -- make(); -- return void 0; ++ }; + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react16.createElement)( @@ -11414,19 +8104,7 @@ diff --git a/client.js b/client.js + if (onOpen) { + onOpen(id); + return; - } -- const timer = setTimeout(make, wait); -- return () => clearTimeout(timer); -- }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); -- leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { -- if (visible) { -- setPhase("in"); -- return void 0; ++ } + if (typeof window !== "undefined") window.location.hash = projectHash(id); + }, [onOpen]); + const [expanded, setExpanded] = (0, import_react16.useState)(false); @@ -11441,41 +8119,9 @@ diff --git a/client.js b/client.js + setProjects(list); + setError(""); + } catch (failure) { -+ setProjects([]); -+ setError(failure?.message ?? String(failure)); - } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); -- return () => clearTimeout(timer); -- }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { -- if (phase !== null) return; -- endedAt.current = Date.now(); -- setShown(null); -- const before = shift.current; -- shift.current = null; -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (before === null || !scroller || reducedMotion()) return; -- const delta = before - scroller.scrollTop; -- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); -- }, [phase]); -- (0, import_react7.useEffect)(() => { -- if (!running) actions.clearLive(sessionId); -- }, [running, sessionId]); -- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { -- if (phase !== "in") return void 0; -- const timer = setInterval(() => setNow(Date.now()), 1e4); -- return () => clearInterval(timer); -- }, [phase]); -- const host = useRunningHost(running); -- if (!host || phase === null || !shown) return null; -- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; -- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( ++ setProjects([]); ++ setError(failure?.message ?? String(failure)); ++ } + }, [read]); + (0, import_react16.useEffect)(() => { + if (!expanded) return; @@ -11491,30 +8137,7 @@ diff --git a/client.js b/client.js + }; + const listed = expanded && wide; + return (0, import_react16.createElement)( - "div", -- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, -- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react7.createElement)( -- "span", -- { className: "bt-typing-text" }, -- (0, import_react7.createElement)( -- "span", -- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, -- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), -- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null -- ) -- ) -- ), host); --} -- --// src/client/dsh-offer.js --var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); --var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; --var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); --function showsDshRow(dsh, app) { -- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -- if (dsh.kind === "npm") return true; -- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ "div", + { className: "bt-projects" }, + (0, import_react16.createElement)( + "button", @@ -11549,11 +8172,9 @@ diff --git a/client.js b/client.js + } + }) + ); - } - - // src/client/sidebar.js --var import_react8 = require("react"); --var import_react_dom3 = require("react-dom"); ++} ++ ++// src/client/sidebar.js +var import_react17 = require("react"); +var import_react_dom4 = require("react-dom"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -11763,7 +8384,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, +@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -11825,7 +8446,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -11838,7 +8459,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -11880,7 +8501,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -11968,7 +8589,7 @@ diff --git a/client.js b/client.js useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); useLandOnMainBot(roster, useSessions, actions); const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -@@ -5045,42 +5045,258 @@ function TeamSidebar(props) { +@@ -5045,39 +5045,43 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; @@ -12029,151 +8650,163 @@ diff --git a/client.js b/client.js + const [dshError, setDshError] = (0, import_react17.useState)(""); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { -+ if (!open) return void 0; -+ const onDown = (event) => { -+ if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -+ setOpen(false); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ setOpen(false); -+ button.current?.focus(); -+ } -+ }; -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); -+ }; -+ }, [open]); -+ const phase = update?.phase ?? "idle"; -+ const dsh = update?.dsh; -+ const dshPhase = dsh?.phase ?? "idle"; -+ const dshRow = showsDshRow(dsh, update?.app); -+ if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; -+ const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; -+ const toggle = () => { -+ const rect = button.current?.getBoundingClientRect(); -+ if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); -+ setOpen((value) => !value); -+ }; -+ const start = () => { -+ setError(""); -+ void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); -+ }; -+ const startDsh = () => { -+ setDshError(""); -+ void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); -+ }; + if (!open) return void 0; + const onDown = (event) => { + if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; +@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) { + setDshError(""); + void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); + }; +- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); + const label = phase === "installing" ? t("Updating FlyBot core…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("FlyBot core {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("FlyBot core {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); -+ const message = error || (phase === "failed" ? update.error : ""); -+ const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); + const message = error || (phase === "failed" ? update.error : ""); + const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); +- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); +- return (0, import_react8.createElement)( +- import_react8.Fragment, + const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); + return (0, import_react17.createElement)( + import_react17.Fragment, -+ null, + null, +- (0, import_react8.createElement)("button", { + (0, import_react17.createElement)("button", { -+ ref: button, -+ type: "button", -+ className: "bt-round bt-update", -+ "data-phase": cloudPhase, -+ "aria-label": label, -+ title: label, -+ "aria-haspopup": "dialog", -+ "aria-expanded": open, -+ onClick: toggle + ref: button, + type: "button", + className: "bt-round bt-update", +@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) { + "aria-haspopup": "dialog", + "aria-expanded": open, + onClick: toggle +- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)( -+ "div", + "div", +- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, +- update?.available || phase !== "idle" ? (0, import_react8.createElement)( +- import_react8.Fragment, + { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box }, + update?.available || phase !== "idle" ? (0, import_react17.createElement)( + import_react17.Fragment, -+ null, + null, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), +- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, +- (0, import_react8.createElement)( + (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), + message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react17.createElement)( -+ "div", -+ { className: "bt-update-actions" }, + "div", + { className: "bt-update-actions" }, +- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, +- phase === "installed" ? null : (0, import_react8.createElement)("button", { + update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react17.createElement)("button", { -+ type: "button", -+ className: "bt-update-go", -+ disabled: phase === "installing", -+ onClick: start -+ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) -+ ) -+ ) : null, + type: "button", + className: "bt-update-go", + disabled: phase === "installing", +@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) { + }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) + ) + ) : null, +- dshRow ? (0, import_react8.createElement)( + dshRow ? (0, import_react17.createElement)( -+ "div", -+ { className: "bt-update-dsh" }, + "div", + { className: "bt-update-dsh" }, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), +- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, +- (0, import_react8.createElement)( + (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText), + dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, + (0, import_react17.createElement)( -+ "div", -+ { className: "bt-update-actions" }, + "div", + { className: "bt-update-actions" }, +- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { +- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { + dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { + }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", { -+ type: "button", -+ className: "bt-update-go", -+ disabled: dshPhase === "installing", -+ onClick: startDsh -+ }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) -+ ) -+ ) : null -+ ), document.body) : null -+ ); -+} + type: "button", + className: "bt-update-go", + disabled: dshPhase === "installing", +@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { + ), document.body) : null + ); + } +-function ConnectPlugins({ wide, actions }) { +- const tipId = (0, import_react8.useId)(); +- const [tip, setTip] = (0, import_react8.useState)(false); +- const timer = (0, import_react8.useRef)(0); +- const button = (0, import_react8.useRef)(null); +- const [box, setBox] = (0, import_react8.useState)(null); +- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); +function ConnectApps({ wide, actions }) { -+ if (!wide) return null; + if (!wide) return null; +- const show = () => { +- clearTimeout(timer.current); +- timer.current = setTimeout(() => { +- const rect = button.current?.getBoundingClientRect(); +- if (!rect) return; +- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); +- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); +- setTip(true); +- }, 250); +- }; +- const hide = () => { +- clearTimeout(timer.current); +- setTip(false); +- }; +- return (0, import_react8.createElement)( + return (0, import_react17.createElement)( -+ "span", -+ { className: "bt-connect-wrap" }, + "span", + { className: "bt-connect-wrap" }, +- (0, import_react8.createElement)( + (0, import_react17.createElement)( -+ "button", + "button", +- { +- ref: button, +- type: "button", +- className: "bt-connect bt-connect-foot", +- "aria-describedby": tip ? tipId : void 0, +- onMouseEnter: show, +- onMouseLeave: hide, +- onFocus: show, +- onBlur: hide, +- onClick: () => actions.selectPanel("plugins") +- }, +- (0, import_react8.createElement)(PlugIcon), +- t("Connect plugins"), +- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") +- ), +- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( +- "span", +- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, +- t("Plugin compatibility is not fully verified.") +- ), document.body) : null + { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, + (0, import_react17.createElement)(PlugIcon), + t("Connect apps") + ) -+ ); -+} -+function YouAvatar() { + ); + } + function YouAvatar() { +- return (0, import_react8.createElement)( + return (0, import_react17.createElement)( -+ "span", -+ { className: "bt-you-wrap" }, + "span", + { className: "bt-you-wrap" }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), +- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) -+ ); -+} -+var openShellSettings = null; -+var openShellOnboarding = null; -+var shellShortcuts = null; -+function holdShellShortcuts(shortcuts) { -+ shellShortcuts = shortcuts; -+ return () => { -+ if (shellShortcuts === shortcuts) shellShortcuts = null; -+ }; -+} -+function openHarnessSettings() { -+ requestAnimationFrame(() => { -+ if (openShellSettings) { -+ openShellSettings(); -+ return; -+ } -+ const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); -+ if (desktop) openThroughMenu(desktop); -+ else pressSettingsShortcut(); -+ }); -+} -+function openHarnessOnboarding(id) { -+ requestAnimationFrame(() => { -+ if (openShellOnboarding) openShellOnboarding(id); -+ else openHarnessSettings(); -+ }); -+} + ); + } + var openShellSettings = null; +@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { + else openHarnessSettings(); + }); + } +var MODELS_STEP_ID = "flybot-models"; +var MODELS_STEP_ORDER = 1e3; +function ModelsSettingsStep({ explicit, complete, openSection }) { @@ -12186,285 +8819,179 @@ diff --git a/client.js b/client.js +function openHarnessModels() { + openHarnessOnboarding(MODELS_STEP_ID); +} -+function openThroughMenu(trigger) { -+ const before = new Set(document.querySelectorAll('[role="menu"]')); -+ trigger.click(); -+ let frames = 0; -+ const look = () => { -+ const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); -+ const item = menu?.querySelector('button[role="menuitem"]'); -+ if (item) item.click(); -+ else if (++frames < 30) requestAnimationFrame(look); -+ else pressSettingsShortcut(); -+ }; -+ requestAnimationFrame(look); -+} -+function pressSettingsShortcut() { -+ const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; -+ if (!binding) return; -+ const held = new Set(binding.modifiers); -+ window.dispatchEvent(new KeyboardEvent("keydown", { -+ code: binding.code, -+ key: binding.code === "Comma" ? "," : "", -+ bubbles: true, -+ cancelable: true, -+ ctrlKey: held.has("control"), -+ altKey: held.has("alt"), -+ shiftKey: held.has("shift"), -+ metaKey: held.has("meta") -+ })); -+} -+function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { + function openThroughMenu(trigger) { + const before = new Set(document.querySelectorAll('[role="menu"]')); + trigger.click(); +@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() { + })); + } + function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { +- const [open, setOpen] = (0, import_react8.useState)(false); +- (0, import_react8.useLayoutEffect)(() => { + const [open, setOpen] = (0, import_react17.useState)(false); + (0, import_react17.useLayoutEffect)(() => { -+ if (!seat) return void 0; -+ actions.seatAccount(true); -+ return () => actions.seatAccount(false); -+ }, [seat]); + if (!seat) return void 0; + actions.seatAccount(true); + return () => actions.seatAccount(false); + }, [seat]); +- const [box, setBox] = (0, import_react8.useState)(null); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { + const [box, setBox] = (0, import_react17.useState)(null); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { -+ if (settingsOpen) setOpen(false); -+ }, [settingsOpen]); + if (settingsOpen) setOpen(false); + }, [settingsOpen]); +- (0, import_react8.useEffect)(() => { + (0, import_react17.useEffect)(() => { -+ if (!openSettings) return void 0; -+ openShellSettings = openSettings; -+ return () => { -+ if (openShellSettings === openSettings) openShellSettings = null; -+ }; -+ }, [openSettings]); + if (!openSettings) return void 0; + openShellSettings = openSettings; + return () => { + if (openShellSettings === openSettings) openShellSettings = null; + }; + }, [openSettings]); +- (0, import_react8.useEffect)(() => { + (0, import_react17.useEffect)(() => { -+ if (!openOnboarding) return void 0; -+ openShellOnboarding = openOnboarding; -+ return () => { -+ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; -+ }; -+ }, [openOnboarding]); + if (!openOnboarding) return void 0; + openShellOnboarding = openOnboarding; + return () => { + if (openShellOnboarding === openOnboarding) openShellOnboarding = null; + }; + }, [openOnboarding]); +- (0, import_react8.useEffect)(() => { + (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { -- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -+ if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; - setOpen(false); - }; - const onKey = (event) => { -@@ -5097,412 +5097,1330 @@ function UpdateButton({ useUpdate, actions }) { - window.removeEventListener("keydown", onKey, true); - }; - }, [open]); -- const phase = update?.phase ?? "idle"; -- const dsh = update?.dsh; -- const dshPhase = dsh?.phase ?? "idle"; -- const dshRow = showsDshRow(dsh, update?.app); -- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; -- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; - const toggle = () => { - const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); -+ if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); - setOpen((value) => !value); - }; -- const start = () => { -- setError(""); -- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); -- }; -- const startDsh = () => { -- setDshError(""); -- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); -- }; -- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); -- const message = error || (phase === "failed" ? update.error : ""); -- const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); -- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); -- return (0, import_react8.createElement)( -- import_react8.Fragment, -+ const groups = [ -+ [ -+ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], + if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; +@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi + const groups = [ + [ + [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], +- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], +- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], +- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] + [t("Connect apps"), ConnectorIcon, () => actions.openConnectors()], + [t("Plugins"), PlugIcon, () => actions.selectPanel("plugins")], + [t("Scheduled tasks"), ClockIcon, () => actions.openSettings("schedules")], + [t("Settings"), GearIcon, () => actions.openSettings("general")] -+ ], -+ [ -+ [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] -+ ] -+ ]; + ], + [ + [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] + ] + ]; +- return (0, import_react8.createElement)( +- import_react8.Fragment, + return (0, import_react17.createElement)( + import_react17.Fragment, null, -- (0, import_react8.createElement)("button", { -- ref: button, -- type: "button", -- className: "bt-round bt-update", -- "data-phase": cloudPhase, -- "aria-label": label, -- title: label, -- "aria-haspopup": "dialog", -- "aria-expanded": open, -- onClick: toggle -- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( +- (0, import_react8.createElement)( + (0, import_react17.createElement)( -+ "button", -+ { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, + "button", + { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) -+ ), + ), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)( "div", -- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, -- update?.available || phase !== "idle" ? (0, import_react8.createElement)( + { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, +- groups.map((group, index) => (0, import_react8.createElement)( - import_react8.Fragment, -- null, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), -- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -- (0, import_react8.createElement)( -- "div", -- { className: "bt-update-actions" }, -- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -- phase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: phase === "installing", -- onClick: start -- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) -- ) -- ) : null, -- dshRow ? (0, import_react8.createElement)( -+ { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, + (0, import_react17.createElement)(FlyBotAccountHeader, { actions }), + groups.map((group, index) => (0, import_react17.createElement)( + import_react17.Fragment, -+ { key: index }, + { key: index }, +- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { + index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", { -+ key: label, -+ type: "button", -+ role: "menuitem", -+ onClick: () => { -+ setOpen(false); -+ void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); -+ } + key: label, + type: "button", + role: "menuitem", +@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi + setOpen(false); + void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); + } +- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) -+ )) -+ ), document.body) : null -+ ); -+} -+function useLandOnMainBot(roster, useSessions, actions) { -+ const mainView = useSessions((list) => { -+ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); -+ return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; -+ }); -+ const lastBot = (0, import_react17.useRef)(null); -+ (0, import_react17.useEffect)(() => { -+ if (!roster.ready || !roster.mainBotId) return void 0; -+ const [id, blank3] = mainView.split("|"); -+ const bot = id === "" ? void 0 : roster.byId[id]; -+ if (bot !== void 0) { -+ lastBot.current = { botId: bot.id, sessionId: id }; -+ if (currentPart(bot) !== id) actions.landOn(bot.id); -+ return void 0; -+ } -+ if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { -+ lastBot.current = null; -+ return void 0; -+ } -+ const last = lastBot.current; + )) + ), document.body) : null + ); +@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) { + const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); + return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; + }); +- const lastBot = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { ++ const lastBot = (0, import_react17.useRef)(null); ++ (0, import_react17.useEffect)(() => { + if (!roster.ready || !roster.mainBotId) return void 0; + const [id, blank3] = mainView.split("|"); + const bot = id === "" ? void 0 : roster.byId[id]; +@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) { + return void 0; + } + const last = lastBot.current; +- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); + const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400); -+ return () => clearTimeout(timer); -+ }, [mainView, roster]); -+} -+function FooterAccount(props) { -+ const seated = props.useUi((value) => value.accountSeated === true); -+ if (seated) return null; + return () => clearTimeout(timer); + }, [mainView, roster]); + } + function FooterAccount(props) { + const seated = props.useUi((value) => value.accountSeated === true); + if (seated) return null; +- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); + return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false })); -+} -+function BotReturn({ wide, useSurface, actions }) { -+ const mode = useSurface((value) => value.mode); -+ if (mode !== "agent") return null; + } + function BotReturn({ wide, useSurface, actions }) { + const mode = useSurface((value) => value.mode); + if (mode !== "agent") return null; +- return (0, import_react8.createElement)( + return (0, import_react17.createElement)( -+ "button", + "button", +- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, +- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), +- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null + { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, + (0, import_react17.createElement)(FlyBotMark, { size: 20 }), + wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null -+ ); -+} -+var RETURN_CSS = ` -+.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} -+.bt-return:hover{background:rgba(127,127,127,.14)} -+.bt-return:active{transform:scale(.96)} + ); + } + var RETURN_CSS = ` + .bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} + .bt-return:hover{background:rgba(127,127,127,.14)} + .bt-return:active{transform:scale(.96)} +-.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} +-.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} +.bt-return .bt-mark,.bt-return .bt-flybot-mark{display:inline-flex;flex:none;line-height:0} +.bt-return .bt-mark svg,.bt-return .bt-flybot-mark{width:20px;height:20px;overflow:visible} -+/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ -+.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} -+.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+`; -+function BrandMark({ size = 24 }) { + /* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ + .bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} + .bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + `; + function BrandMark({ size = 24 }) { +- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); + return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size })); -+} -+function BrandName() { + } + function BrandName() { +- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); + return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); -+} -+var SIDEBAR_CSS = ` -+.bt-side-top .bt-brand{cursor:default;padding-left:6px} -+.bt-side-top .bt-brand:hover{background:none} -+.bt-brand .bt-mark{flex:none} -+.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} -+.bt-rail .bt-row{position:relative} -+.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} -+.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} -+[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} -+[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} -+.bt-foot-account{order:-1;flex:none;display:inline-flex} -+[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} -+[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} -+[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} -+ -+/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a -+ loop while the install runs. */ -+.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} -+.bt-round.bt-update svg{width:17px;height:17px} -+.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} -+@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} -+.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} -+@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} -+.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} + } + var SIDEBAR_CSS = ` + .bt-side-top .bt-brand{cursor:default;padding-left:6px} +@@ -5456,7 +5456,7 @@ var SIDEBAR_CSS = ` + .bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} + @keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} + .bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +.bt-update-pop{position:fixed;z-index:130;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} -+@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} -+.bt-update-title{font-size:14px;line-height:20px;font-weight:600} -+.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} -+.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} -+.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} -+.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} -+.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} -+.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} -+.bt-update-go:hover:not(:disabled){opacity:.9} -+.bt-update-go:active:not(:disabled){transform:scale(.96)} -+.bt-update-go:disabled{opacity:.6;cursor:default} -+.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} -+@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} -+ -+/* The DSH Agent row and its expanding Session list. The black whale is the -+ harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ -+.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} -+.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} -+@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} -+.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} -+.bt-agent-sub[data-open]{grid-template-rows:1fr} -+.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} -+.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} -+.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} -+.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} -+.bt-agent-new{color:var(--bt-ink-3)} -+.bt-agent-new svg{width:14px;height:14px;flex:none} -+.bt-agent-edit{padding:3px 10px 3px 4px} -+.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} -+.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} -+@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} -+@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} + @keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} + .bt-update-title{font-size:14px;line-height:20px;font-weight:600} + .bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} +@@ -5491,18 +5491,1177 @@ var SIDEBAR_CSS = ` + .bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} + @keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} + @media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} + +.bt-row-wrap{position:relative;display:flex;align-items:center;width:100%;min-width:0} +.bt-row-wrap>.bt-row{flex:1 1 auto;min-width:0;width:100%} @@ -12561,23 +9088,10 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-astack" }, + (0, import_react18.createElement)( - "div", -- { className: "bt-update-dsh" }, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), -- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -- (0, import_react8.createElement)( ++ "div", + { className: "bt-msg" }, + (0, import_react18.createElement)( - "div", -- { className: "bt-update-actions" }, -- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { -- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: dshPhase === "installing", -- onClick: startDsh -- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) ++ "div", + { className: "bt-msg-line" }, + text2 ? (0, import_react18.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react18.createElement)( + "div", @@ -12586,10 +9100,7 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("span", null), + (0, import_react18.createElement)("span", null) + ) - ) -- ) : null -- ), document.body) : null -- ); ++ ) + ) + ) + ), host); @@ -12603,8 +9114,8 @@ diff --git a/client.js b/client.js +.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s} +@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}} +@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} -+`; -+ + `; + +// src/client/project-board.js +var import_react19 = require("react"); +var PROJECT_BOARD_CSS = ` @@ -12799,24 +9310,7 @@ diff --git a/client.js b/client.js + emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." + } + }; - } --function ConnectPlugins({ wide, actions }) { -- const tipId = (0, import_react8.useId)(); -- const [tip, setTip] = (0, import_react8.useState)(false); -- const timer = (0, import_react8.useRef)(0); -- const button = (0, import_react8.useRef)(null); -- const [box, setBox] = (0, import_react8.useState)(null); -- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); -- if (!wide) return null; -- const show = () => { -- clearTimeout(timer.current); -- timer.current = setTimeout(() => { -- const rect = button.current?.getBoundingClientRect(); -- if (!rect) return; -- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); -- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); -- setTip(true); -- }, 250); ++} +function projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics } = {}) { + const isRow = (row) => row !== null && typeof row === "object" && text(row.id) !== ""; + const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); @@ -12849,10 +9343,7 @@ diff --git a/client.js b/client.js + speaksFor: text(row.speaksFor) || null, + board: boardOfRow(row, task) + }; - }; -- const hide = () => { -- clearTimeout(timer.current); -- setTip(false); ++ }; + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], @@ -12889,28 +9380,7 @@ diff --git a/client.js b/client.js + emptyText: "No cards yet.", + record: projectBoardRecord({ decisions, meetings }), + metrics: metricsView(metrics, { tasks }) - }; -- return (0, import_react8.createElement)( -- "span", -- { className: "bt-connect-wrap" }, -- (0, import_react8.createElement)( -- "button", -- { -- ref: button, -- type: "button", -- className: "bt-connect bt-connect-foot", -- "aria-describedby": tip ? tipId : void 0, -- onMouseEnter: show, -- onMouseLeave: hide, -- onFocus: show, -- onBlur: hide, -- onClick: () => actions.selectPanel("plugins") -- }, -- (0, import_react8.createElement)(PlugIcon), -- t("Connect plugins"), -- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") -- ), -- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ }; +} +function projectRowNode(row, onOpen) { + const open = row.board === null ? void 0 : () => onOpen?.(row.board); @@ -12931,10 +9401,7 @@ diff --git a/client.js b/client.js + }, + (0, import_react19.createElement)("span", { className: "bt-project-column" }, t(row.column)), + (0, import_react19.createElement)( - "span", -- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, -- t("Plugin compatibility is not fully verified.") -- ), document.body) : null ++ "span", + { className: "bt-project-title-wrap" }, + (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), + // The audit text the row already carries (#1730): shown inside the card detail, never a @@ -12947,14 +9414,8 @@ diff --git a/client.js b/client.js + row.speaksFor === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), + row.priority === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-priority" }, row.priority), + row.runId === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) - ); - } --function YouAvatar() { -- return (0, import_react8.createElement)( -- "span", -- { className: "bt-you-wrap" }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ ); ++} +function ProjectBoardRows({ view, onOpen }) { + if (view.empty) return (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); + return view.groups.map((group) => (0, import_react19.createElement)( @@ -12971,15 +9432,8 @@ diff --git a/client.js b/client.js + { className: "bt-project-group bt-project-done", "data-group": "done" }, + (0, import_react19.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), + view.done.rows.map((row) => projectRowNode(row, onOpen)) - ); - } --var openShellSettings = null; --var openShellOnboarding = null; --var shellShortcuts = null; --function holdShellShortcuts(shortcuts) { -- shellShortcuts = shortcuts; -- return () => { -- if (shellShortcuts === shortcuts) shellShortcuts = null; ++ ); ++} +function waitedLabel(ms) { + if (!Number.isFinite(ms) || ms < 0) return null; + if (ms < 6e4) return `${ms === 0 ? 0 : Math.max(1, Math.floor(ms / 1e3))}s`; @@ -13033,18 +9487,8 @@ diff --git a/client.js b/client.js + running, + empty: source === null, + emptyText: "No metrics read yet." - }; - } --function openHarnessSettings() { -- requestAnimationFrame(() => { -- if (openShellSettings) { -- openShellSettings(); -- return; -- } -- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); -- if (desktop) openThroughMenu(desktop); -- else pressSettingsShortcut(); -- }); ++ }; ++} +function ProjectMetrics({ view }) { + return (0, import_react19.createElement)( + "section", @@ -13062,12 +9506,7 @@ diff --git a/client.js b/client.js + )) + ) + ); - } --function openHarnessOnboarding(id) { -- requestAnimationFrame(() => { -- if (openShellOnboarding) openShellOnboarding(id); -- else openHarnessSettings(); -- }); ++} +function ProjectRunningTable({ view, onOpen }) { + if (view.running.length === 0) return (0, import_react19.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react19.createElement)("h4", null, t("Running tasks")), (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); + return (0, import_react19.createElement)( @@ -13106,17 +9545,7 @@ diff --git a/client.js b/client.js + ); + }) + ); - } --function openThroughMenu(trigger) { -- const before = new Set(document.querySelectorAll('[role="menu"]')); -- trigger.click(); -- let frames = 0; -- const look = () => { -- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); -- const item = menu?.querySelector('button[role="menuitem"]'); -- if (item) item.click(); -- else if (++frames < 30) requestAnimationFrame(look); -- else pressSettingsShortcut(); ++} +function ProjectQueueGroups({ view, onOpen }) { + return view.queues.map((entry) => (0, import_react19.createElement)( + "section", @@ -13218,23 +9647,8 @@ diff --git a/client.js b/client.js + // `project-read` record answers no rank, and the panel never re-derives the host's policy. + role: text(project?.role), + choices: idList(project?.choices) - }; -- requestAnimationFrame(look); - } --function pressSettingsShortcut() { -- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; -- if (!binding) return; -- const held = new Set(binding.modifiers); -- window.dispatchEvent(new KeyboardEvent("keydown", { -- code: binding.code, -- key: binding.code === "Comma" ? "," : "", -- bubbles: true, -- cancelable: true, -- ctrlKey: held.has("control"), -- altKey: held.has("alt"), -- shiftKey: held.has("shift"), -- metaKey: held.has("meta") -- })); ++ }; ++} +var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react19.createElement)( + "div", + { @@ -13318,44 +9732,7 @@ diff --git a/client.js b/client.js + ) + ) + ); - } --function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { -- const [open, setOpen] = (0, import_react8.useState)(false); -- (0, import_react8.useLayoutEffect)(() => { -- if (!seat) return void 0; -- actions.seatAccount(true); -- return () => actions.seatAccount(false); -- }, [seat]); -- const [box, setBox] = (0, import_react8.useState)(null); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (settingsOpen) setOpen(false); -- }, [settingsOpen]); -- (0, import_react8.useEffect)(() => { -- if (!openSettings) return void 0; -- openShellSettings = openSettings; -- return () => { -- if (openShellSettings === openSettings) openShellSettings = null; -- }; -- }, [openSettings]); -- (0, import_react8.useEffect)(() => { -- if (!openOnboarding) return void 0; -- openShellOnboarding = openOnboarding; -- return () => { -- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; -- }; -- }, [openOnboarding]); -- (0, import_react8.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -- setOpen(false); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setOpen(false); -- button.current?.focus(); ++} +function dossierRowOf(team, botId) { + const id = text(botId); + if (id === "") return null; @@ -13426,41 +9803,11 @@ diff --git a/client.js b/client.js + target = botIdOf(record?.project?.assistant); + } catch { + target = ""; - } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [open]); -- const toggle = () => { -- const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); -- setOpen((value) => !value); ++ } + if (target !== "") setAgentId(target); + } + if (target !== "") onOpen?.(target); - }; -- const groups = [ -- [ -- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], -- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], -- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], -- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] -- ], -- [ -- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] -- ] -- ]; -- return (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)( -- "button", -- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ }; + if (id === "") return null; + return (0, import_react19.createElement)("button", { + type: "button", @@ -13496,8 +9843,7 @@ diff --git a/client.js b/client.js + { className: "bt-project-actions" }, + (0, import_react19.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), + (0, import_react19.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) - ), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ), + (0, import_react19.createElement)(ProjectMetrics, { view: view.metrics }), + (0, import_react19.createElement)(ProjectRunningTable, { view: view.metrics, onOpen }), + (0, import_react19.createElement)(ProjectBoardRows, { view, onOpen }), @@ -13533,14 +9879,7 @@ diff --git a/client.js b/client.js + { className: "bt-project-panel", "data-panel": "project-settings" }, + (0, import_react19.createElement)("h3", null, t("Project settings")), + (0, import_react19.createElement)( - "div", -- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, -- groups.map((group, index) => (0, import_react8.createElement)( -- import_react8.Fragment, -- { key: index }, -- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { -- key: label, ++ "div", + { className: "bt-project-field" }, + (0, import_react19.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")), + (0, import_react19.createElement)( @@ -13580,20 +9919,16 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-project-row-actions" }, + (0, import_react19.createElement)("button", { - type: "button", -- role: "menuitem", ++ type: "button", + className: "bt-soft bt-soft-sm", + "data-action": "project-memory-save", + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. - onClick: () => { -- setOpen(false); -- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); ++ onClick: () => { + if (noteSaveDisabled) return; + run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")); - } -- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) ++ } + }, t("Save memory")) + ) + ), @@ -13643,8 +9978,7 @@ diff --git a/client.js b/client.js + { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, + (0, import_react19.createElement)("span", null, `@${name}`), + (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) - )) -- ), document.body) : null ++ )) + ), + owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, + (0, import_react19.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")), @@ -13653,36 +9987,21 @@ diff --git a/client.js b/client.js + { className: "bt-project-row-actions" }, + (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) - ); - } --function useLandOnMainBot(roster, useSessions, actions) { -- const mainView = useSessions((list) => { -- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); -- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; -- }); -- const lastBot = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (!roster.ready || !roster.mainBotId) return void 0; -- const [id, blank3] = mainView.split("|"); -- const bot = id === "" ? void 0 : roster.byId[id]; -- if (bot !== void 0) { -- lastBot.current = { botId: bot.id, sessionId: id }; -- if (currentPart(bot) !== id) actions.landOn(bot.id); -- return void 0; -- } -- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { -- lastBot.current = null; -+async function readProjectBoard(read, wanted) { ++ ); ++} ++async function readProjectBoard(read, wanted, cached) { + const listing = await read("projects"); + const projects = Array.isArray(listing?.projects) ? listing.projects : []; + const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); + if (project === void 0) return { project: null, rows: [], done: [], canManage: false, tasks: [], team: { rows: [] } }; + const zone = typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function" ? void 0 : Intl.DateTimeFormat().resolvedOptions().timeZone; ++ const seeded = cached !== null && typeof cached === "object" && cached.team !== void 0; ++ const teamAnswer = seeded ? Promise.resolve(cached.team) : Promise.resolve().then(() => read("project-team", { id: project.id })).catch(() => null); + const [record, board, taskList, team] = await Promise.all([ + read("project-read", { id: project.id }), + read("project-board", { id: project.id, timeZone: zone }), + read("tasks", {}), -+ read("project-team", { id: project.id }) ++ teamAnswer + ]); + const tasks = Array.isArray(taskList) ? taskList : taskList?.tasks ?? []; + const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null }; @@ -13736,29 +10055,16 @@ diff --git a/client.js b/client.js + if (typeof window === "undefined") return void 0; + if (!hash.startsWith("#project")) { + setState(null); - return void 0; - } -- const last = lastBot.current; -- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); -- return () => clearTimeout(timer); -- }, [mainView, roster]); --} --function FooterAccount(props) { -- const seated = props.useUi((value) => value.accountSeated === true); -- if (seated) return null; -- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); --} --function BotReturn({ wide, useSurface, actions }) { -- const mode = useSurface((value) => value.mode); -- if (mode !== "agent") return null; -- return (0, import_react8.createElement)( -- "button", -- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, -- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), -- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null ++ return void 0; ++ } + const wanted = hash.slice("#project".length).replace(/^=/, ""); + let gone = false; -+ const loadBoard = () => readProjectBoard(read, wanted); ++ let teamAnswer; ++ const loadBoard = async () => { ++ const next = await readProjectBoard(read, wanted, teamAnswer === void 0 ? void 0 : { team: teamAnswer }); ++ if (next.project !== null) teamAnswer = next.team; ++ return next; ++ }; + const refresh = async (first) => { + try { + const next = await loadBoard(); @@ -13844,85 +10150,9 @@ diff --git a/client.js b/client.js + }) + ) + ) - ); - } --var RETURN_CSS = ` --.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} --.bt-return:hover{background:rgba(127,127,127,.14)} --.bt-return:active{transform:scale(.96)} --.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} --.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} --/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ --.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} --.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --`; --function BrandMark({ size = 24 }) { -- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); --} --function BrandName() { -- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); --} --var SIDEBAR_CSS = ` --.bt-side-top .bt-brand{cursor:default;padding-left:6px} --.bt-side-top .bt-brand:hover{background:none} --.bt-brand .bt-mark{flex:none} --.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} --.bt-rail .bt-row{position:relative} --.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} --.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} --[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} --.bt-foot-account{order:-1;flex:none;display:inline-flex} --[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} -- --/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a -- loop while the install runs. */ --.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-round.bt-update svg{width:17px;height:17px} --.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} --@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} --.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} --@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} --.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} --.bt-update-title{font-size:14px;line-height:20px;font-weight:600} --.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} --.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} --.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} --.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} --.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} --.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} --.bt-update-go:hover:not(:disabled){opacity:.9} --.bt-update-go:active:not(:disabled){transform:scale(.96)} --.bt-update-go:disabled{opacity:.6;cursor:default} --.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} --@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} -- --/* The DSH Agent row and its expanding Session list. The black whale is the -- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ --.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} --@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} --.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-agent-sub[data-open]{grid-template-rows:1fr} --.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} --.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} --.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} --.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} --.bt-agent-new{color:var(--bt-ink-3)} --.bt-agent-new svg{width:14px;height:14px;flex:none} --.bt-agent-edit{padding:3px 10px 3px 4px} --.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} --.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} --@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} --@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} --`; - ++ ); ++} ++ // src/client/header.js -var import_react9 = require("react"); -var import_react_dom4 = require("react-dom"); @@ -14029,14 +10259,15 @@ diff --git a/client.js b/client.js - (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), - (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), - (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) +- ), +- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), +- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), +- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) + bot ? (0, import_react20.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react20.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react20.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), + (0, import_react20.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react20.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react20.createElement)(GoArrow)) - ), -- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), -- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) ++ ), + (0, import_react20.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + projectAgent === null ? null : (0, import_react20.createElement)(ProjectAgentEntry, { + projectId: projectAgent.projectId, @@ -17004,7 +13235,7 @@ diff --git a/client.js b/client.js const isMainBot = isMainOf(roster, bot.id); const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; const isTile = roster.mainBotId === bot.id; -@@ -8011,149 +8011,812 @@ function BotSettingsForm({ bot, roster, actions, run }) { +@@ -8011,112 +8011,116 @@ function BotSettingsForm({ bot, roster, actions, run }) { if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -17109,59 +13340,73 @@ diff --git a/client.js b/client.js + (0, import_react30.createElement)(BotSchedules, { bot, roster, actions }), + (0, import_react30.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), + (0, import_react30.createElement)( -+ Card, -+ { title: t("Main Bot") }, + Card, + { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + onlyMain ? (0, import_react30.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( + (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "span", + { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) + (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -+ ), + ), +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) +- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) + (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) + ) : (0, import_react30.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) -+ ), -+ // The Main Bot tile is always first and always shown, like in the context menu. + ), + // The Main Bot tile is always first and always shown, like in the context menu. +- isTile ? null : (0, import_react18.createElement)( + isTile ? null : (0, import_react30.createElement)( -+ Card, -+ { title: t("Sidebar") }, + Card, + { title: t("Sidebar") }, +- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), +- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) + (0, import_react30.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), + isMainBot ? null : (0, import_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -+ ), + ), +- (0, import_react18.createElement)( + (0, import_react30.createElement)( -+ Card, + Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( + { title: t("More") }, + (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, + "button", + { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), + (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) -+ ), + ), +- isMainBot ? null : (0, import_react18.createElement)( + isMainBot ? null : (0, import_react30.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + "button", + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react18.createElement)( + (0, import_react30.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "span", + { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) + (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), + (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -+ ), + ), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) -+ ) -+ ) -+ ); -+} -+var BOT_SETTINGS_CSS = ` -+.bt-bs{display:flex;flex-direction:column;gap:18px} -+.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} -+.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} -+.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-bs .bt-avedit{left:0;transform:none} -+.bt-bs-row{min-height:48px} -+label.bt-bs-row{cursor:text} -+.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} + ) + ) + ); +@@ -8130,9 +8130,17 @@ var BOT_SETTINGS_CSS = ` + .bt-bs .bt-avedit{left:0;transform:none} + .bt-bs-row{min-height:48px} + label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} + .bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to + break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test + connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under @@ -17169,29 +13414,16 @@ diff --git a/client.js b/client.js +.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} +.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} -+.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} + .bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +${MODEL_TEST_CSS} -+.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} -+.bt-bs-input:hover{background:var(--bt-hover)} -+.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -+.bt-bs-input::placeholder{color:var(--bt-ink-3)} -+.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} -+.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} -+.bt-bs-area:hover{background:var(--bt-active)} -+.bt-bs-danger .bt-bs-label{color:#c21d2e} -+.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} -+.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} -+.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} -+.bt-switch[aria-checked=true]{background:var(--bt-accent)} -+.bt-switch:disabled{opacity:.45;cursor:default} -+.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} -+.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} -+.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} -+.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} -+.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} -+.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} -+@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} + .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} + .bt-bs-input:hover{background:var(--bt-hover)} + .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +@@ -8153,8 +8153,659 @@ label.bt-bs-row{cursor:text} + .bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} + .bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} + @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below + 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and + keeps its own words. Nothing is clipped to fit a width that is not there. */ @@ -17434,20 +13666,13 @@ diff --git a/client.js b/client.js + people2.map((person) => (0, import_react31.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) + ) : null, + face ? null : (0, import_react31.createElement)( - Card, -- { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( ++ Card, + { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, + humans.map((person) => (0, import_react31.createElement)( - "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( ++ "div", + { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, + (0, import_react31.createElement)( - "span", -- { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ "span", + { className: "bt-gs-who" }, + (0, import_react31.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), + (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Human")), @@ -17487,9 +13712,7 @@ diff --git a/client.js b/client.js + // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is + // labelled as such; a Bot with no Bot Social identity can never be written to at all. + room2.origin === "face-to-face" ? null : (0, import_react31.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) - ), -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) ++ ), + // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. + room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") + )), @@ -17534,43 +13757,26 @@ diff --git a/client.js b/client.js + // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after + // the note on who leads, so that note stays with the members it is about (#892). + room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react31.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) - ), -- // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react18.createElement)( ++ ), + (0, import_react31.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), + (0, import_react31.createElement)( - Card, - { title: t("Sidebar") }, -- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) ++ Card, ++ { title: t("Sidebar") }, + (0, import_react31.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), + (0, import_react31.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react31.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) -- ), -- isMainBot ? null : (0, import_react18.createElement)( ++ Card, + { title: t("More") }, + (0, import_react31.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + (0, import_react31.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react31.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), + (0, import_react31.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) - ), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ ), + (0, import_react31.createElement)("span", { className: "bt-chevron" }, (0, import_react31.createElement)(ChevronRightIcon)) + ) + ) @@ -17670,8 +13876,8 @@ diff --git a/client.js b/client.js +.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} +.bt-bs-stack .bt-seg{align-self:stretch} +@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} -+`; -+ + `; + +// src/client/palette.js +var import_react32 = require("react"); +var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -17866,46 +14072,14 @@ diff --git a/client.js b/client.js + ); + }) + )) - ) - ) - ); - } --var BOT_SETTINGS_CSS = ` --.bt-bs{display:flex;flex-direction:column;gap:18px} --.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} --.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} --.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-bs .bt-avedit{left:0;transform:none} --.bt-bs-row{min-height:48px} --label.bt-bs-row{cursor:text} --.bt-bs-row>.bt-set-copy{flex:1 1 auto} --.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} --.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} --.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} --.bt-bs-input:hover{background:var(--bt-hover)} --.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} --.bt-bs-input::placeholder{color:var(--bt-ink-3)} --.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} --.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} --.bt-bs-area:hover{background:var(--bt-active)} --.bt-bs-danger .bt-bs-label{color:#c21d2e} --.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} --.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} --.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} --.bt-switch[aria-checked=true]{background:var(--bt-accent)} --.bt-switch:disabled{opacity:.45;cursor:default} --.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} --.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} --.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} --.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} --.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} --.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} --@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} --`; - ++ ) ++ ) ++ ); ++} ++ // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { + const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); @@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { } function startModel(roster) { @@ -18981,49 +15155,6 @@ diff --git a/client.js b/client.js - { className: "bt-bs-hero" }, - (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - (0, import_react23.createElement)( -- "div", -- { className: "bt-bs-who" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), -- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Profile") }, -- (0, import_react23.createElement)( -- Row, -- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) -- ), -- (0, import_react23.createElement)( -- Row, -- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Who answers") }, -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": mode === id, -- disabled: id === "admin" && !admin, -- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, -- onClick: () => { -- if (mode !== id) void update({ mode: id }); -- } -- }, t(label)))), -- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react23.createElement)( + { className: "bt-wf-palette" }, + (0, import_react37.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), + locals.map((bot) => (0, import_react37.createElement)("button", { @@ -19060,71 +15191,12 @@ diff --git a/client.js b/client.js + { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, + (0, import_react37.createElement)( "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length }) }, -- members.map((member) => { -- const leads = room2.admin === member.id; -- return (0, import_react23.createElement)( +- { className: "bt-bs-who" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), +- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) + { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, + (0, import_react37.createElement)( - "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null -- ); -- }), -- adding ? [ -- ...outside.map((bot) => (0, import_react23.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ add: [bot.id] }); -- } }, -- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host checks for two members on every change, even a rename. -- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") -- )) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), -- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Manage") }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -- ), -- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) ++ "div", + { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, + (0, import_react37.createElement)( + "svg", @@ -19211,43 +15283,14 @@ diff --git a/client.js b/client.js + { className: "bt-wf-copy" }, + (0, import_react37.createElement)("span", { className: "bt-wf-name" }, node.name), + (0, import_react37.createElement)("span", { className: "bt-wf-role" }, node.role ?? "") -+ ) -+ ); -+ }) -+ ) - ) - ) - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react23.useState)(null); -- const [members, setMembers] = (0, import_react23.useState)([]); -- const [name, setName] = (0, import_react23.useState)(""); -- const [busy, setBusy] = (0, import_react23.useState)(false); -- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const pickAdmin = (id) => { -- setAdmin(id); -- setMembers((list) => list.filter((item) => item !== id)); -- }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -- setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -- }; -- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); -- return (0, import_react23.createElement)( ++ ) ++ ); ++ }) ++ ) + ) ++ ) ++ ); ++} +function Row2({ item, source, onOpen, onRename, onDelete }) { + return (0, import_react37.createElement)( + "button", @@ -19263,7 +15306,14 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react37.createElement)("span", null, item.name), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") -+ ), + ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Profile") }, +- (0, import_react23.createElement)( +- Row, +- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, +- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) + source === "own" ? (0, import_react37.createElement)( + "span", + { className: "bt-set-actions" }, @@ -19302,19 +15352,11 @@ diff --git a/client.js b/client.js + const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); + const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; + return (0, import_react37.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( ++ "div", + { className: "bt-wf-page" }, + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")), + error ? (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) ++ "div", + { className: "bt-error", role: "alert" }, + alertCopy(error), + phase === "error" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null @@ -19338,7 +15380,11 @@ diff --git a/client.js b/client.js + return (0, import_react37.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); + }) + ) -+ ), + ), +- (0, import_react23.createElement)( +- Row, +- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, +- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) + (0, import_react37.createElement)( + "section", + { className: "bt-set-section" }, @@ -19351,18 +15397,34 @@ diff --git a/client.js b/client.js + ) ) - ), -- admin === null ? null : (0, import_react23.createElement)( +- (0, import_react23.createElement)( +- Card, +- { title: t("Who answers") }, +- (0, import_react23.createElement)( + ) : null, + graph ? (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-wf-editor" }, + (0, import_react37.createElement)( "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { +- key: id, +- type: "button", +- "aria-pressed": mode === id, +- disabled: id === "admin" && !admin, +- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, +- onClick: () => { +- if (mode !== id) void update({ mode: id }); +- } +- }, t(label)))), +- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Working folder") }, +- (0, import_react23.createElement)( + { className: "bt-wf-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), + showUse ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, @@ -19398,42 +15460,153 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Members · {count}", { count: members.length }) }, +- members.map((member) => { +- const leads = room2.admin === member.id; +- return (0, import_react23.createElement)( +- "div", +- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, +- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), +- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), +- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null +- ), +- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), +- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null +- ); +- }), +- adding ? [ +- ...outside.map((bot) => (0, import_react23.createElement)( +- "button", +- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { +- setAdding(false); +- void update({ add: [bot.id] }); +- } }, +- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) +- )), +- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) +- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, +- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( +- "span", +- { className: "bt-set-hint" }, +- // The host checks for two members on every change, even a rename. +- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") +- )) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), +- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) +- ), +- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) -- ), -- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), -- (0, import_react23.createElement)( -+ ) -+ ); -+} + ) + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function DeleteModal({ workflow, onClose, onConfirm }) { + return (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) +- ) +- ), +- admin === null ? null : (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), +- (0, import_react23.createElement)( + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ "div", + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, + (0, import_react37.createElement)("h3", null, t("Delete workflow")), + (0, import_react37.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, +- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) -+ ) + ) +- ), +- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) +- ), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ) ); } @@ -19855,7 +16028,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick +@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -21411,7 +17584,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false +@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -21829,22 +18002,10 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11026,256 +11026,20 @@ function migrateStorage(storage = globalThis.localStorage) { - } +@@ -11038,244 +11038,8 @@ function migrateStorage(storage = globalThis.localStorage) { } - try { -- migrateBrowsers(storage); -- } catch { -- } -- try { -- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -- } -- } catch { -- } --} -- + } + -// src/client/member-status.js -var import_react30 = require("react"); -var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -22010,16 +18171,7 @@ diff --git a/client.js b/client.js - (0, import_react30.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), - entry.approvals.map((approval) => (0, import_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) - ); -+ migrateBrowsers(storage); -+ } catch { -+ } -+ try { -+ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -+ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -+ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -+ } -+ } catch { - } +- } - if (entry.state !== "work") return null; - const where = entry.doing.roomId === room2.id ? "" : entry.doing.roomId ? t("in {name}", { name: roster.roomsById[entry.doing.roomId]?.name ?? t("a group chat") }) : t("in its own chat"); - const stop = () => { @@ -22042,7 +18194,7 @@ diff --git a/client.js b/client.js - const entries2 = room2 ? memberWork(room2, work) : []; - const now = useNow2(entries2.some((entry) => entry.state !== "idle")); - return { entries: entries2, now }; - } +-} -var MEMBER_CSS = ` -.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} -.bt-mw-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} @@ -22078,7 +18230,7 @@ diff --git a/client.js b/client.js -@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} -@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} -`; - +- // src/client/details-drawer.js -var import_react31 = require("react"); -function transcriptOf(list, id, name, hiddenTurns = []) { @@ -22218,8 +18370,11 @@ diff --git a/client.js b/client.js useMorph(body, open); - return (0, import_react31.createElement)( + return (0, import_react44.createElement)( -+ "div", -+ null, + "div", + null, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), +- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), +- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null + (0, import_react44.createElement)("div", { className: "bt-section-title" }, t(title)), + (0, import_react44.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), + long ? (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null @@ -22272,11 +18427,8 @@ diff --git a/client.js b/client.js + const people2 = facePeople(room2, roster); + const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); + return (0, import_react44.createElement)( - "div", - null, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ "div", ++ null, + (0, import_react44.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), + (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), + (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react44.createElement)(FacePersonRow, { @@ -22782,7 +18934,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -22839,7 +18991,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) } }) ), @@ -22987,7 +19139,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roste if (event.target === event.currentTarget) setLanded(true); } }, @@ -23750,7 +19902,7 @@ diff --git a/client.js b/client.js + const { turns, steps } = params; + return text2 === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text2; + }; - } ++} +function statsDock(entriesOf, id = STATS_ID) { + const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id); + function StatsDock(props) { @@ -23760,7 +19912,7 @@ diff --git a/client.js b/client.js + return (0, import_react49.createElement)(entry.component, { ...props, t: t2 }); + } + return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} }; -+} + } +var statsDocks = (entriesOf) => STATS_IDS.map((id) => ({ id, ...statsDock(entriesOf, id) })); // src/client/index.js @@ -24226,7 +20378,7 @@ diff --git a/client.js b/client.js shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice), shadow("conversation.composer", { priority: -10, -@@ -14074,8 +14074,17 @@ function apply(ctx) { +@@ -14074,9 +14074,18 @@ function apply(ctx) { shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock), shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock), shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock), @@ -24234,7 +20386,7 @@ diff --git a/client.js b/client.js + shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock), + shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock) ]; -+ ctx.effect(() => { + ctx.effect(() => { + const style = installStyles(TURN_FAILURES_CSS, "turn-failures"); + const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react50.createElement)(TurnFailures, { ...props, work }) }); + return () => { @@ -24242,6 +20394,7 @@ diff --git a/client.js b/client.js + style.remove(); + }; + }, "ds-bot: dead turn lines"); - ctx.effect(() => { ++ ctx.effect(() => { const style = installStyles(RETURN_CSS, "return"); return () => style.remove(); + }, "ds-bot: return styles"); diff --git a/patches/ds-bot/files/src/client/project-board.js b/patches/ds-bot/files/src/client/project-board.js index 4f8e0cbbc..a3ba1a31e 100644 --- a/patches/ds-bot/files/src/client/project-board.js +++ b/patches/ds-bot/files/src/client/project-board.js @@ -866,7 +866,11 @@ export function ProjectMembersPanel({ project, rows, onClose } = {}) { // #1864: one read for the whole board, exported so a test can prove the dossier comes from a single // `project-team` call. `read` is the channel seam (`call` in the app). -export async function readProjectBoard(read, wanted) { +// #1900: the dossier is the one answer in that read which does not change on a five-second clock, so +// the caller hands back the answer it already has (`cached.team`) and the read skips the call: the +// board reads it once when it opens and every poll after that draws that same answer. The other +// three channels are read every time — they are the reason a poll exists. +export async function readProjectBoard(read, wanted, cached) { const listing = await read('projects') const projects = Array.isArray(listing?.projects) ? listing.projects : [] const project = wanted === '' ? projects[0] : projects.find(item => item.id === wanted) @@ -874,11 +878,20 @@ export async function readProjectBoard(read, wanted) { const zone = typeof Intl === 'undefined' || typeof Intl.DateTimeFormat !== 'function' ? undefined : Intl.DateTimeFormat().resolvedOptions().timeZone + const seeded = cached !== null && typeof cached === 'object' && cached.team !== undefined + // #1900: a dossier that throws is the team area's loss, not the board's. The rejection used to + // reject the whole `Promise.all`, and the first paint turned that into the empty state — a broken + // persona file made a Project that exists read as "No Project yet", which is the one answer + // `:873` reserves for a Project that really is gone. Rows, `done`, tasks and queues still draw; + // only `team` gives up, as `{ rows: [] }` below. + const teamAnswer = seeded + ? Promise.resolve(cached.team) + : Promise.resolve().then(() => read('project-team', { id: project.id })).catch(() => null) const [record, board, taskList, team] = await Promise.all([ read('project-read', { id: project.id }), read('project-board', { id: project.id, timeZone: zone }), read('tasks', {}), - read('project-team', { id: project.id }), + teamAnswer, ]) const tasks = Array.isArray(taskList) ? taskList : (taskList?.tasks ?? []) // The groups are the same read as the rows, so the header cannot disagree with the cards. @@ -951,7 +964,17 @@ export function ProjectBoardOverlay({ actions, call: request } = {}) { let gone = false // #1864: the board, its record, its rows, the task texts and the team dossier are one read path, // and the dossier comes from the single `project-team` call that read makes. - const loadBoard = () => readProjectBoard(read, wanted) + // #1900: that one dossier answer is held for as long as this board is open. The effect re-runs + // per `#project` entry, so re-opening the board reads the dossier again and a persona or memory + // edit lands then; the five-second poll only draws the answer the open read already took. + let teamAnswer + const loadBoard = async () => { + const next = await readProjectBoard(read, wanted, teamAnswer === undefined ? undefined : { team: teamAnswer }) + // A board with no Project took no dossier read at all, so it seeds nothing: the first poll + // that does find the Project reads the dossier itself instead of caching the empty answer. + if (next.project !== null) teamAnswer = next.team + return next + } const refresh = async (first) => { try { const next = await loadBoard() diff --git a/patches/ds-bot/files/test/project-board-panels.test.js b/patches/ds-bot/files/test/project-board-panels.test.js index 6f4a2de11..cbe1fcdbb 100644 --- a/patches/ds-bot/files/test/project-board-panels.test.js +++ b/patches/ds-bot/files/test/project-board-panels.test.js @@ -537,6 +537,53 @@ test('the board reads the dossier from exactly one project-team call', async () assert.deepEqual(opened.map(entry => entry.botId), ['bot-chief']) }) +// #1900: the dossier only changes when someone edits a persona or a memory file, so it is read +// once, when the board opens, and the five-second poll (#1814/#1824) draws that answer instead of +// asking again. The poll was re-reading the whole team dossier — host side a `memoryDigest` per row, +// ~16 KB every 5 s — to receive an answer that had not changed. +test('#1900: the five-second poll reuses the dossier answer the open read took', async () => { + resetSlots() + const calls = [] + const read = async (endpoint) => { + calls.push(endpoint) + if (endpoint === 'projects') return { projects: [{ id: 'project-1', name: 'FlyBot', role: 'owner', choices: [] }] } + if (endpoint === 'project-read') return { project: projectRecord(), canManage: true } + if (endpoint === 'project-board') return boardAnswer() + if (endpoint === 'tasks') return [] + if (endpoint === 'project-team') return { rows: [dossier(), null, { ...dossier(), botId: 'bot-dev', name: 'Dev' }] } + throw new Error(`unexpected endpoint ${endpoint}`) + } + const count = () => calls.filter(endpoint => endpoint === 'project-team').length + const opened = await readProjectBoard(read, 'project-1') + assert.equal(count(), 1, 'opening the board reads the dossier once') + const polled = await readProjectBoard(read, 'project-1', { team: opened.team }) + assert.equal(count(), 1, 'the poll does not ask for the dossier again') + assert.equal(polled.team.rows.length, 3, 'the poll keeps drawing the one answer') + assert.deepEqual(polled.team.rows.map(row => row?.botId), opened.team.rows.map(row => row?.botId)) +}) + +// #1900: a dossier that fails is not the board failing. It keeps its rows, its `done` group and its +// tasks, and the team area draws empty — the empty-project answer (`project-board.js:873`) stays +// reserved for a Project that really is absent, so a broken dossier cannot paint "No Project yet". +test('#1900: a dossier read that throws leaves the board drawn and the team empty', async () => { + resetSlots() + const read = async (endpoint) => { + if (endpoint === 'projects') return { projects: [{ id: 'project-1', name: 'FlyBot', role: 'owner', choices: [] }] } + if (endpoint === 'project-read') return { project: projectRecord(), canManage: true } + if (endpoint === 'project-board') return { ...boardAnswer(), tasks: [{ id: 'row-1' }], done: [{ id: 'done-1' }] } + if (endpoint === 'tasks') return [{ id: 'task-1' }] + if (endpoint === 'project-team') throw new Error('the dossier is unreadable this tick') + throw new Error(`unexpected endpoint ${endpoint}`) + } + const state = await readProjectBoard(read, 'project-1') + assert.equal(state.project?.id, 'project-1', 'the Project still draws: no empty state') + assert.equal(state.rows.length, 1) + assert.equal(state.done.length, 1) + assert.equal(state.tasks.length, 1) + assert.deepEqual(Object.keys(state.team).sort(), ['rows'], 'the team area is the one thing that gives up') + assert.equal(state.team.rows.length, 0) +}) + // #1892: the role pool is a rank list, not a Bot the board holds a dossier answer for. // `project-team` answers rows keyed by the Project's own Bots, so a pool click asked for a row the // host never sent and drew an empty panel. A pool entry now reads `@id` beside its rank; the Chief From ab8653218bcfda425ffd28a946cf7e039b47acc0 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 31 Oct 2026 15:05:45 +0800 Subject: [PATCH 2/6] #1900: say in the core-client doc that the dossier is read once per open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/subsystems/core-client.md owns core/src/client/project-board.js, and the arch-docs gate asks for it: the dossier paragraph now says the project-team answer is taken once when the board opens (readProjectBoard's cached.team), that the five-second poll draws it, and that a failing dossier leaves the board drawn with team { rows: [] }. The four-reads paragraph says the other three reads still repeat. Docs-not-needed: not claimed — the subsystem doc is updated instead. --- docs/subsystems/core-client.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index b8ad95e90..3dbeb1cd7 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -323,7 +323,13 @@ instructions, role and model), **Memory** (the `MEMORY.md` head and the topic na (the attached `skill` capability names). The panel never re-fetches per section, and a `null` row — the host's answer for a deleted Bot — shows *Not set* with nothing invented. `readProjectBoard` keeps the dossier on the same read as the rows, so the board and the panel cannot disagree, and the -"my agent" row uses the same panel. A role-pool entry draws none: since #1892 it is plain `@id` +"my agent" row uses the same panel. That read is taken once, when the board opens (#1900): the export +takes the previous answer back as `cached.team` and skips the `project-team` call when it has one, so +the five-second refresh draws the dossier the open read took instead of re-reading every persona and +memory head on a timer — the answer only changes when someone edits one of those files, and a re-open +reads it again. A dossier that throws is non-fatal: rows, `done`, tasks and queues still draw, `team` +becomes `{ rows: [] }`, and the empty-project answer stays reserved for a Project that is really +absent. A role-pool entry draws none: since #1892 it is plain `@id` beside its rank, because `project-team` answers rows keyed by the Project's own Bots and the old control asked for a row the host never sent. The entry in the owner's own Bot chat is `ProjectAgentEntry` in the header: the roster already carries `bot.projectId`, the entry reads @@ -357,7 +363,8 @@ carried in and has no board of its own). A row with a board is a button: it writ group and clears the hash, so a reload does not reopen the same board. A row without a board (or without a task record) is not a button — `role` alone opens nothing. Opening the pane is four host reads, never a per-Bot read: `projects`, `project-read`, `project-board`, and the `tasks` list the rows are joined -against. Since #1814 they repeat every five seconds while the pane is open, because what a card has +against; since #1900 the fourth, `project-team`, is read once at that open and the poll below draws +that answer instead. The other three repeat every five seconds while the pane is open, because what a card has waited and whether its lease is live are computed by the host at read time: a lease that expires during the visit turns the card `stalled` on its own instead of freezing the board. The first read failing answers the empty state as it did; a poll that fails keeps the board the last good read drew. Polling From 26eaa112fa9a34447767b502f7fa73726d473910 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 31 Oct 2026 15:07:15 +0800 Subject: [PATCH 3/6] #1900: run generate on CI, where git exists This box has no git, so patches/ds-bot/0008-client-bundle.patch was written by hand and check passes, but canonical (git-only) cannot be satisfied from here. This throwaway workflow runs node scripts/ds-bot-patches.mjs generate and commits its output to this branch; it is removed in the next commit. Docs-not-needed: the subsystem doc is updated in the parent commit. --- .github/workflows/regen-1900-patches.yml | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/regen-1900-patches.yml diff --git a/.github/workflows/regen-1900-patches.yml b/.github/workflows/regen-1900-patches.yml new file mode 100644 index 000000000..d9a1bade0 --- /dev/null +++ b/.github/workflows/regen-1900-patches.yml @@ -0,0 +1,27 @@ +name: Regen 1900 patches +on: + push: + branches: [dev/1900-dossier-off-poll] +permissions: + contents: write +jobs: + regen: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: dev/1900-dossier-off-poll + fetch-depth: 0 + - name: generate + run: node scripts/ds-bot-patches.mjs generate + - name: commit the series git would write + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if git diff --quiet -- patches/ds-bot; then echo "nothing to regenerate"; exit 0; fi + git add patches/ds-bot + git commit -m "Regenerate patches/ds-bot from core/ (generate, #1900)" + git push origin HEAD:dev/1900-dossier-off-poll + - name: canonical + run: node scripts/ds-bot-patches.mjs canonical From 24f2d5355bea3cba13cb78926a48f005d7aaf1e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 31 Oct 2026 07:07:29 +0000 Subject: [PATCH 4/6] Regenerate patches/ds-bot from core/ (generate, #1900) --- patches/ds-bot/0008-client-bundle.patch | 8947 ++++++++++++++++------- 1 file changed, 6400 insertions(+), 2547 deletions(-) diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index d02122982..8c745ad23 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1717,16 +1717,7 @@ diff --git a/client.js b/client.js - "--bt-tag-bg": rgba(mode.text, dark ? 0.09 : 0.06), - "--bt-tag-ink": rgba(mode.text, 0.58) - }; -+// src/client/cluster-order.js -+function clusterOrder(people2) { -+ const list = Array.isArray(people2) ? people2 : []; -+ const remote = (bot) => Boolean(bot?.remote); -+ if (!list.some(remote)) return list; -+ const locals = list.filter((bot) => !remote(bot)); -+ const remotes = list.filter(remote); -+ if (list.length <= 4) return [...remotes, ...locals]; -+ return [...locals, ...remotes]; - } +-} -var declarations = (vars) => Object.entries(vars).map(([name, value]) => `${name}:${value}`).join(";"); -var themeCss = (theme, accent) => `html body{${declarations(themeVars(theme.light, false, accent ?? theme.light.accent))}} -html body[data-ds-dark-theme]{${declarations(themeVars(theme.dark, true, accent ?? theme.dark.accent))}} @@ -1769,7 +1760,16 @@ diff --git a/client.js b/client.js - else delete SHAPES[id]; - refresh(); - }; --} ++// src/client/cluster-order.js ++function clusterOrder(people2) { ++ const list = Array.isArray(people2) ? people2 : []; ++ const remote = (bot) => Boolean(bot?.remote); ++ if (!list.some(remote)) return list; ++ const locals = list.filter((bot) => !remote(bot)); ++ const remotes = list.filter(remote); ++ if (list.length <= 4) return [...remotes, ...locals]; ++ return [...locals, ...remotes]; + } -function registerTheme(id, theme) { - if (typeof id !== "string" || !/^[a-z][\w-]{0,31}$/.test(id)) throw new Error("Theme ids are lowercase words"); - for (const mode of ["light", "dark"]) { @@ -1832,6 +1832,11 @@ diff --git a/client.js b/client.js } return spans; } +- +-// src/client/text.js +-var MENTION_ORIGIN = "https://bots.local/"; +-function linkMentions(text, bots, selfId) { +- if (!text || !text.includes("@")) return text; +function unmatchedMentions(text2, bots, { references = [] } = {}) { + const value = String(text2 ?? ""); + const skip = [ @@ -1879,11 +1884,9 @@ diff --git a/client.js b/client.js + } + return stripFence(out).replace(/\s+/g, " ").trim(); +} - - // src/client/text.js - var MENTION_ORIGIN = "https://bots.local/"; --function linkMentions(text, bots, selfId) { -- if (!text || !text.includes("@")) return text; ++ ++// src/client/text.js ++var MENTION_ORIGIN = "https://bots.local/"; +function linkMentions(text2, bots, selfId) { + if (!text2 || !text2.includes("@")) return text2; + const ordered = [...bots ?? []].sort((a, b) => Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); @@ -2121,16 +2124,65 @@ diff --git a/client.js b/client.js var formatTokens = (count) => count >= 1e6 ? `${(count / 1e6).toFixed(1)}M` : count >= 1e3 ? `${(count / 1e3).toFixed(1)}K` : String(count); function usageOf(list, ids) { let input = 0; -@@ -1624,9 +1624,982 @@ function usageOf(list, ids) { +@@ -1624,901 +1624,1981 @@ function usageOf(list, ids) { const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } +- +-// src/client/motion.js +-var import_react = require("react"); +-var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; +-function springEasing(stiffness, damping) { +- const w0 = Math.sqrt(stiffness); +- const zeta = damping / (2 * w0); +- const at = (t2) => { +- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); +- const wd = w0 * Math.sqrt(1 - zeta * zeta); +- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); +- }; +- let end = 0.05; +- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; +- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); +- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; +function faceRowTitle(title) { + const text2 = String(title ?? ""); + const named = text2.match(/^Face-to-face · (.+) · (\d{6})$/); + if (named) return `${named[2]} · ${named[1]}`; + return text2.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text2; -+} + } +-var SPRING_SHAPE = springEasing(400, 31.2); +-var SPRING_TAP = springEasing(900, 48); +-var SPRING_LIST = springEasing(484, 35.2); +-var SPRING_STRETCH = springEasing(320, 20); +-var SPRING_POP = springEasing(500, 22); +-var SPRING_LEAD = springEasing(784, 48.2); +-var SPRING_TRAIL = springEasing(289, 29.2); +-var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; +-var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); +-function liveSprings(apply2) { +- const values = /* @__PURE__ */ new Map(); +- let frame = 0; +- let last = 0; +- const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); +- const step = (now) => { +- const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); +- last = now; +- const steps = Math.ceil(dt * 240); +- const h34 = dt / steps; +- let moving = false; +- for (const value of values.values()) { +- for (let index = 0; index < steps; index += 1) { +- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; +- value.x += value.v * h34; +- } +- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { +- value.x = value.to; +- value.v = 0; +- } else moving = true; +- } +- apply2(read()); +- frame = moving ? requestAnimationFrame(step) : 0; +- }; +function modelOf(bot, roster) { + const ref = bot.model ?? bot.appliedModel; + if (!ref) return void 0; @@ -2145,28 +2197,84 @@ diff --git a/client.js b/client.js +function awayGroups(groups, roomsById) { + return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face"); +} - --// src/client/motion.js ++ +// src/client/sources.js +function createSource(initial) { + let value = initial; + const listeners2 = /* @__PURE__ */ new Set(); -+ return { + return { +- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, +- set(targets, curves, jump) { +- for (const [key, to] of Object.entries(targets)) { +- const value = values.get(key); +- const { stiffness: k, damping: c } = curves[key]; +- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); +- else Object.assign(value, { to, k, c }); +- } +- if (jump) { +- cancelAnimationFrame(frame); +- frame = 0; +- apply2(read()); +- return; +- } +- if (!frame) { +- last = performance.now(); +- frame = requestAnimationFrame(step); +- } + getSnapshot: () => value, + subscribe: (listener) => { + listeners2.add(listener); + return () => { + listeners2.delete(listener); + }; -+ }, + }, +- stop: () => cancelAnimationFrame(frame) + set(next) { + const resolved = typeof next === "function" ? next(value) : next; + if (resolved === value) return; + value = resolved; + for (const listener of [...listeners2]) listener(); + } -+ }; -+} + }; + } +-var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; +-function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { +- const ownBox = (0, import_react.useRef)(null); +- const box = given ?? ownBox; +- const thumb = (0, import_react.useRef)(null); +- const own = (0, import_react.useRef)(null); +- if (own.current === null) { +- own.current = { +- key: void 0, +- node: null, +- observer: null, +- springs: liveSprings(({ start, end }) => { +- const knob = thumb.current; +- if (!knob) return; +- knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; +- knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; +- }) +- }; +- } +- const place = (jump) => { +- const container = box.current; +- const knob = thumb.current; +- if (!container || !knob) return; +- const chosen = container.querySelector(selector); +- if (!chosen) { +- knob.dataset.hidden = ""; +- return; +- } +- const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; +- const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); +- const { springs } = own.current; +- const before = springs.target(); +- const hidden = knob.dataset.hidden !== void 0; +- if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; +- delete knob.dataset.hidden; +- const forward = before === null || start >= before.start; +- springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); +- container.dataset.glide = ""; +var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], humans: [], self: null, agents: [], exchangeTurns: {}, remoteTypedTurns: {}, remoteAwaiting: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }; +var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id); +function indexRoster(value) { @@ -2179,7 +2287,26 @@ diff --git a/client.js b/client.js + if (prev && prev !== bot && !prev.remote && bot?.remote) return; + if (prev && prev !== bot && prev.id === id) return; + byId[id] = bot; -+ }; + }; +- (0, import_react.useLayoutEffect)(() => { +- const state = own.current; +- const node = box.current; +- if (node === state.node && key === state.key) return; +- const fresh = node !== state.node; +- state.key = key; +- if (fresh) { +- state.observer?.disconnect(); +- state.node = node; +- state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; +- state.observer?.observe(node); +- } +- place(fresh); +- }); +- (0, import_react.useEffect)(() => () => { +- own.current.observer?.disconnect(); +- own.current.springs.stop(); +- }, []); +- return { box, thumb }; + for (const bot of value.bots) { + for (const id of [...bot.parts ?? [], bot.sessionId ?? bot.id, bot.id]) claim(bot, id); + } @@ -2192,20 +2319,75 @@ diff --git a/client.js b/client.js + for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id; + } + return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }; -+} + } +-function Segmented({ label, value, className, children }) { +- const glide = useGlide(value); +- return (0, import_react.createElement)( +- "div", +- { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, +- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- children +- ); +function rosterBot(roster, id) { + if (id === void 0 || id === null || id === "") return void 0; + const list = roster?.bots ?? []; + const own = list.find((bot) => bot && !bot.remote && (bot.id === id || bot.sessionId === id || (bot.parts ?? []).includes(id))); + if (own) return own; + return roster?.byId?.[id]; -+} + } +-function useFlip(list, key, selector, id = (node) => node.dataset.flip) { +- const tops = (0, import_react.useRef)(null); +- (0, import_react.useLayoutEffect)(() => { +- const container = list.current; +- if (!container) return; +- const next = /* @__PURE__ */ new Map(); +- const moving = tops.current !== null && !reducedMotion(); +- for (const node of container.querySelectorAll(selector)) { +- const name = id(node); +- const top = node.offsetTop; +- next.set(name, top); +- if (!moving) continue; +- const before = tops.current.get(name); +- if (before === void 0) { +- node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); +- } else if (Math.abs(before - top) > 0.5) { +- node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); +- } +- } +- tops.current = next; +- }, [key]); +function botByName(bots, name) { + const wanted = String(name ?? "").toLowerCase(); + if (wanted === "") return void 0; + const list = Array.isArray(bots) ? bots : []; + return list.find((bot) => bot && !bot.remote && String(bot.name ?? "").toLowerCase() === wanted) ?? list.find((bot) => bot && String(bot.name ?? "").toLowerCase() === wanted); -+} + } +-function useMorph(box, key) { +- const last = (0, import_react.useRef)({ key, height: null }); +- (0, import_react.useLayoutEffect)(() => { +- const node = box.current; +- const state = last.current; +- if (!node) { +- last.current = { key, height: null }; +- return; +- } +- const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); +- if (key === state.key) { +- if (running.length === 0) state.height = node.offsetHeight; +- return; +- } +- const from = running.length ? node.offsetHeight : state.height; +- for (const animation2 of running) animation2.cancel(); +- const to = node.offsetHeight; +- last.current = { key, height: to }; +- if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; +- node.dataset.morph = ""; +- const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); +- animation.onfinish = () => { +- delete node.dataset.morph; +- }; +- animation.oncancel = () => { +- 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; @@ -2222,7 +2404,7 @@ diff --git a/client.js b/client.js +} + +// src/client/mark.js - var import_react = require("react"); ++var import_react = require("react"); +var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; @@ -2239,7 +2421,8 @@ 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); @@ -2257,7 +2440,9 @@ 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)); @@ -2337,17 +2522,22 @@ 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)(() => { + (0, import_react.useEffect)(() => { +- if (value) { +- setKept(value); + const was = prev.current; + prev.current = state; + if (state !== "idle") { + setSettled(false); -+ return void 0; -+ } + return void 0; + } +- const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms); + if (was === "idle" || was === "alert") return void 0; + setSettled(true); + const timer = setTimeout(() => setSettled(false), 1800); -+ return () => clearTimeout(timer); + return () => clearTimeout(timer); +- }, [value]); +- return [value || kept, !value && Boolean(kept)]; + }, [state]); + return settled; +} @@ -2440,8 +2630,189 @@ 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} +- +-.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}} +- +-.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}} +- +-.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/client/room-invite-preview.js +var INVITE_PREVIEW_NAME_MAX = 60; +function stripInviteMarkers(text2) { @@ -2522,7 +2893,79 @@ 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-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/approval-claims.js +var claims = /* @__PURE__ */ new Map(); +var claimListeners = /* @__PURE__ */ new Set(); @@ -2548,13 +2991,77 @@ diff --git a/client.js b/client.js + claimListeners.delete(fn); + }; +} -+ + +-.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} +// src/shared/approval-waiting.js +var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]); +function isApprovalWaiting(status) { + return WAITING_STATUSES.includes(status); +} -+ + +-.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} +// 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). @@ -2574,7 +3081,41 @@ diff --git a/client.js b/client.js + const said = `${record.error ?? ""} ${record.code ?? ""}`; + return !TERMINAL.some((reason) => reason.test(said)); +}; -+ + +-.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} +// src/client/flybot-slots.js +var import_react2 = require("react"); +var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload); @@ -2868,7 +3409,57 @@ 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; } -+ + +-/* 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 @@ -2897,7 +3488,66 @@ 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 = (text2) => { + const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text2); @@ -2924,7 +3574,21 @@ 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) => { @@ -2937,7 +3601,15 @@ 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 }) { @@ -2967,7 +3639,66 @@ 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 = { @@ -3072,7 +3803,114 @@ 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; @@ -3099,115 +3937,270 @@ 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} +-.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} +// src/client/motion.js +var import_react4 = require("react"); - var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; - function springEasing(stiffness, damping) { - const w0 = Math.sqrt(stiffness); -@@ -1659,12 +1659,12 @@ function liveSprings(apply2) { - const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); - last = now; - const steps = Math.ceil(dt * 240); -- const h34 = dt / steps; ++var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; ++function springEasing(stiffness, damping) { ++ const w0 = Math.sqrt(stiffness); ++ const zeta = damping / (2 * w0); ++ const at = (t2) => { ++ if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); ++ const wd = w0 * Math.sqrt(1 - zeta * zeta); ++ return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); ++ }; ++ let end = 0.05; ++ while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; ++ const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); ++ return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; ++} ++var SPRING_SHAPE = springEasing(400, 31.2); ++var SPRING_TAP = springEasing(900, 48); ++var SPRING_LIST = springEasing(484, 35.2); ++var SPRING_STRETCH = springEasing(320, 20); ++var SPRING_POP = springEasing(500, 22); ++var SPRING_LEAD = springEasing(784, 48.2); ++var SPRING_TRAIL = springEasing(289, 29.2); ++var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; ++var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); ++function liveSprings(apply2) { ++ const values = /* @__PURE__ */ new Map(); ++ let frame = 0; ++ let last = 0; ++ const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); ++ const step = (now) => { ++ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); ++ last = now; ++ const steps = Math.ceil(dt * 240); + const h50 = dt / steps; - let moving = false; - for (const value of values.values()) { - for (let index = 0; index < steps; index += 1) { -- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; -- value.x += value.v * h34; ++ let moving = false; ++ for (const value of values.values()) { ++ for (let index = 0; index < steps; index += 1) { + value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h50; + value.x += value.v * h50; - } - if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { - value.x = value.to; -@@ -1699,10 +1699,10 @@ function liveSprings(apply2) { - } - var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; - function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { -- const ownBox = (0, import_react.useRef)(null); ++ } ++ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { ++ value.x = value.to; ++ value.v = 0; ++ } else moving = true; ++ } ++ apply2(read()); ++ frame = moving ? requestAnimationFrame(step) : 0; ++ }; ++ return { ++ target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, ++ set(targets, curves, jump) { ++ for (const [key, to] of Object.entries(targets)) { ++ const value = values.get(key); ++ const { stiffness: k, damping: c } = curves[key]; ++ if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); ++ else Object.assign(value, { to, k, c }); ++ } ++ if (jump) { ++ cancelAnimationFrame(frame); ++ frame = 0; ++ apply2(read()); ++ return; ++ } ++ if (!frame) { ++ last = performance.now(); ++ frame = requestAnimationFrame(step); ++ } ++ }, ++ stop: () => cancelAnimationFrame(frame) ++ }; ++} ++var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; ++function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { + const ownBox = (0, import_react4.useRef)(null); - const box = given ?? ownBox; -- const thumb = (0, import_react.useRef)(null); -- const own = (0, import_react.useRef)(null); ++ const box = given ?? ownBox; + const thumb = (0, import_react4.useRef)(null); + const own = (0, import_react4.useRef)(null); - if (own.current === null) { - own.current = { - key: void 0, -@@ -1736,7 +1736,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); - container.dataset.glide = ""; - }; -- (0, import_react.useLayoutEffect)(() => { ++ if (own.current === null) { ++ own.current = { ++ key: void 0, ++ node: null, ++ observer: null, ++ springs: liveSprings(({ start, end }) => { ++ const knob = thumb.current; ++ if (!knob) return; ++ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; ++ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; ++ }) ++ }; ++ } ++ const place = (jump) => { ++ const container = box.current; ++ const knob = thumb.current; ++ if (!container || !knob) return; ++ const chosen = container.querySelector(selector); ++ if (!chosen) { ++ knob.dataset.hidden = ""; ++ return; ++ } ++ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; ++ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); ++ const { springs } = own.current; ++ const before = springs.target(); ++ const hidden = knob.dataset.hidden !== void 0; ++ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; ++ delete knob.dataset.hidden; ++ const forward = before === null || start >= before.start; ++ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); ++ container.dataset.glide = ""; ++ }; + (0, import_react4.useLayoutEffect)(() => { - const state = own.current; - const node = box.current; - if (node === state.node && key === state.key) return; -@@ -1750,7 +1750,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - } - place(fresh); - }); -- (0, import_react.useEffect)(() => () => { ++ const state = own.current; ++ const node = box.current; ++ if (node === state.node && key === state.key) return; ++ const fresh = node !== state.node; ++ state.key = key; ++ if (fresh) { ++ state.observer?.disconnect(); ++ state.node = node; ++ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; ++ state.observer?.observe(node); ++ } ++ place(fresh); ++ }); + (0, import_react4.useEffect)(() => () => { - own.current.observer?.disconnect(); - own.current.springs.stop(); - }, []); -@@ -1758,16 +1758,16 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - } - function Segmented({ label, value, className, children }) { - const glide = useGlide(value); -- return (0, import_react.createElement)( ++ own.current.observer?.disconnect(); ++ own.current.springs.stop(); ++ }, []); ++ return { box, thumb }; ++} ++function Segmented({ label, value, className, children }) { ++ const glide = useGlide(value); + return (0, import_react4.createElement)( - "div", - { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, -- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ "div", ++ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, + (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - children - ); - } - function useFlip(list, key, selector, id = (node) => node.dataset.flip) { -- const tops = (0, import_react.useRef)(null); -- (0, import_react.useLayoutEffect)(() => { ++ children ++ ); ++} ++function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + const tops = (0, import_react4.useRef)(null); + (0, import_react4.useLayoutEffect)(() => { - const container = list.current; - if (!container) return; - const next = /* @__PURE__ */ new Map(); -@@ -1788,8 +1788,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) { - }, [key]); - } - function useMorph(box, key) { -- const last = (0, import_react.useRef)({ key, height: null }); -- (0, import_react.useLayoutEffect)(() => { ++ const container = list.current; ++ if (!container) return; ++ const next = /* @__PURE__ */ new Map(); ++ const moving = tops.current !== null && !reducedMotion(); ++ for (const node of container.querySelectorAll(selector)) { ++ const name = id(node); ++ const top = node.offsetTop; ++ next.set(name, top); ++ if (!moving) continue; ++ const before = tops.current.get(name); ++ if (before === void 0) { ++ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); ++ } else if (Math.abs(before - top) > 0.5) { ++ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); ++ } ++ } ++ tops.current = next; ++ }, [key]); ++} ++function useMorph(box, key) { + const last = (0, import_react4.useRef)({ key, height: null }); + (0, import_react4.useLayoutEffect)(() => { - const node = box.current; - const state = last.current; - if (!node) { -@@ -1817,8 +1817,8 @@ function useMorph(box, key) { - }); - } - function useLinger(value, ms) { -- const [kept, setKept] = (0, import_react.useState)(value); -- (0, import_react.useEffect)(() => { ++ const node = box.current; ++ const state = last.current; ++ if (!node) { ++ last.current = { key, height: null }; ++ return; ++ } ++ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); ++ if (key === state.key) { ++ if (running.length === 0) state.height = node.offsetHeight; ++ return; ++ } ++ const from = running.length ? node.offsetHeight : state.height; ++ for (const animation2 of running) animation2.cancel(); ++ const to = node.offsetHeight; ++ last.current = { key, height: to }; ++ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; ++ node.dataset.morph = ""; ++ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); ++ animation.onfinish = () => { ++ delete node.dataset.morph; ++ }; ++ animation.oncancel = () => { ++ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; ++ }; ++ }); ++} ++function useLinger(value, ms) { + const [kept, setKept] = (0, import_react4.useState)(value); + (0, import_react4.useEffect)(() => { - 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)} ++ 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)} +.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} -@@ -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-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-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} @@ -3216,14 +4209,83 @@ 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 -@@ -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)} ++@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} +.bt-brand-mark{display:inline-flex;line-height:0} +.bt-flybot-mark{display:block;flex:none} +.bt-flybot-mark .bt-flybot-wing{fill:#8FB2FF} @@ -3232,131 +4294,325 @@ 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-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-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{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} -@@ -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-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-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-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-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}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}} ++@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +@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")}} -@@ -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-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-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} -@@ -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-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-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} -@@ -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-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: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} -@@ -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-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-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} -@@ -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-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};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%)}} --[class*="_centerCol"]{transition:margin-right ${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};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} -@@ -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-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-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} -@@ -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-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: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} -@@ -2237,9 +2237,23 @@ button.bt-q-opt:active{background:var(-- - .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} ++.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. */ +/* #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 @@ -3365,8 +4621,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 @@ -3374,34 +4630,68 @@ 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} -@@ -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{--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-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} -@@ -2288,13 +2288,63 @@ button.bt-set-row:hover{background:var(--bt-hover)} - .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} - .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} - .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} ++/* 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} +/* #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)} @@ -3446,81 +4736,331 @@ 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} -@@ -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} ++ ++/* 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-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. */ -@@ -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} ++ ++/* 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} +.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} -@@ -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-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-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} -@@ -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-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: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} -@@ -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-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{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 -@@ -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-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-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} -@@ -2597,9 +2597,20 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} ++.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} + .bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} +@@ -2595,2185 +2595,4658 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} + :is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} + @media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} `; - var CONVERSATION_CSS = ` - .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} +-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{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;white-space:nowrap} - .bt-think summary::-webkit-details-marker{display:none} +-.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;max-width:100%;min-width:0;white-space:nowrap} ++.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-think-peek{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-3)} @@ -3533,27 +5073,27 @@ diff --git a/client.js b/client.js +.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;overflow-wrap:anywhere;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} -@@ -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%} ++.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} +/* 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 @@ -3573,11 +5113,9 @@ 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. */ --[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} ++[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"]):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. @@ -3593,18 +5131,20 @@ 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} -@@ -2646,8 +2646,19 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - [data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} - [data-chat-running]>span:not([role=status]){display:none!important} - [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} ++[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-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} @@ -3613,212 +5153,224 @@ diff --git a/client.js b/client.js +.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s} +@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}} +@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} - .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))} -@@ -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} ++.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} +[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} -@@ -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} ++[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} +/* 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} -@@ -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} ++[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] [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} -@@ -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-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-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} -@@ -2784,10 +2784,10 @@ div:has(> nav [class*="_marks"]){display:none!important} - .bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} - .bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} - `; --function installStyles(text, tag) { ++.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(text2, tag) { - const style = document.createElement("style"); - style.dataset.dshBot = tag; -- style.textContent = text; ++ const style = document.createElement("style"); ++ style.dataset.dshBot = tag; + style.textContent = text2; - document.head.appendChild(style); - return style; - } -@@ -2807,9 +2807,9 @@ function sheetClassNames(sheet) { - } - return names; - } --function compileFragmentSelectors(text, names) { ++ 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(text2, names) { - const compiled = /* @__PURE__ */ new Map(); -- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { ++ const compiled = /* @__PURE__ */ new Map(); + return text2.replace(FRAGMENT_SELECTOR, (whole, fragment) => { - if (!compiled.has(fragment)) { - const exact = []; - for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -@@ -2818,7 +2818,7 @@ function compileFragmentSelectors(text, names) { - return compiled.get(fragment); - }); - } --function installCompiledStyles(text, tag) { ++ 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(text2, tag) { - const style = installStyles("", tag); - const cache = /* @__PURE__ */ new WeakMap(); - const render = () => { -@@ -2832,7 +2832,7 @@ function installCompiledStyles(text, tag) { - } - for (const name of list) names.add(name); - } -- const next = compileFragmentSelectors(text, names); ++ 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(text2, names); - if (style.textContent !== next) style.textContent = next; - }; - render(); -@@ -2855,226 +2855,204 @@ 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]); --} --var FIDGETS = ["hop", "sway", "stretch", "look"]; --function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { -- const look = given ?? lookOf(bot); -- const ref = (0, import_react2.useRef)(null); -- const maskId = (0, import_react2.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; -- const [poked, setPoked] = (0, import_react2.useState)(false); -- useGaze(ref, gaze); -- const common = { -- ref, -- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", -- "data-shape": look.image ? "image" : shapeId(look.shape), -- "data-state": STATES.includes(state) ? state : "idle", -- "data-live": live ? "" : void 0, -- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, -- "data-gaze": gaze ? "" : void 0, -- "data-poke": pokeable ? "" : void 0, -- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, -- role: label ? "img" : void 0, -- "aria-label": label, -- "aria-hidden": label ? void 0 : true, -- onClick: pokeable ? () => setPoked(true) : void 0, -- onAnimationEnd: poked ? (event) => { -- if (/^bt-poke/.test(event.animationName)) setPoked(false); -- } : void 0 -- }; -- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); -- const box = shapeOf(look.shape).size; -- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); --} --function StarIcon({ size = 8 }) { -- return (0, import_react2.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) -- ); --} --function AdminStar() { -- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); --} --function MainStar() { -- return (0, import_react2.createElement)( -- "svg", -- { viewBox: "0 0 20 20", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) -- ); --} --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 -- ); ++ 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(); ++ }; ++} ++ +// src/shared/face-title.js +var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function isGenericFaceTitle(name) { @@ -3835,14 +5387,7 @@ diff --git a/client.js b/client.js +function isAutoFaceTitle(name) { + const n = String(name ?? "").trim(); + return isGenericFaceTitle(n) || n.startsWith("Face-to-face · "); - } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; ++} +function ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -3853,43 +5398,10 @@ diff --git a/client.js b/client.js + seen.add(key); + const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase(); + rows.push({ owner, sort: handle || key }); - } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; ++ } + rows.sort((a, b) => a.sort.localeCompare(b.sort)); + return rows.slice(0, 3).map((row) => row.owner); - } --function RoomAvatar({ room: room2, roster, size = 36 }) { -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const people = members.length === 1 ? [members[0], null] : members; -- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); -- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); -- if (size <= 20 && people.length > 1) { -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, -- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), -- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null -- ); -- } -- const seats = clusterSeats(Math.min(people.length, 4), size); -- const overflow = people.length > 4 ? people.length - 3 : 0; -- const gap = size / 14; -- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-cluster", style: { width: size, height: size } }, -- seats.map(([x, y, s], index) => { -- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); -- const style = { left: x, top: y, width: s, height: s }; -- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); -- if (overflow > 0 && index === 3) { -- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); -- } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) -- ); ++} +function uniqueOwners(owners) { + const seen = /* @__PURE__ */ new Set(); + const out = []; @@ -3994,135 +5506,116 @@ diff --git a/client.js b/client.js + observer?.disconnect(); + link.remove(); + }; - } - - // src/client/icons.js --var import_react3 = require("react"); ++} ++ ++// src/client/icons.js +var import_react5 = require("react"); - function PlusIcon() { -- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++function PlusIcon() { + return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); - } - function SearchIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++} ++function SearchIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); - } - function PeopleIcon() { -- return (0, import_react3.createElement)( ++} ++function PeopleIcon() { + return (0, import_react5.createElement)( - "svg", - { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), -- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), -- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) ++ "svg", ++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, + (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), + (0, import_react5.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), + (0, import_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) - ); - } - function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); ++ ); ++} ++function ArrowUpIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); - } - function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( ++} ++function MicIcon({ size = 16 }) { + return (0, import_react5.createElement)( - "svg", - { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) ++ "svg", ++ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, + (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), + (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) - ); - } - function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( ++ ); ++} ++function WaveIcon({ size = 16 }) { + return (0, import_react5.createElement)( - "svg", - { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) ++ "svg", ++ { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) - ); - } - function ShareIcon() { -- return (0, import_react3.createElement)( ++ ); ++} ++function ShareIcon() { + return (0, import_react5.createElement)( - "svg", - { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) ++ "svg", ++ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react5.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) - ); - } - function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++ ); ++} ++function CloseIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); - } --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); ++} +var ChevronLeftIcon = () => (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +var ChevronRightIcon = () => (0, import_react5.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); - var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( ++var lineIcon = (paths) => function LineIcon() { + return (0, import_react5.createElement)( - "svg", - { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) ++ "svg", ++ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + paths.map((d, index) => (0, import_react5.createElement)("path", { key: index, d })) - ); - }; - var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; - function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( ++ ); ++}; ++var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; ++function CloudDownloadIcon({ done = false }) { + return (0, import_react5.createElement)( - "svg", - { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) ++ "svg", ++ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react5.createElement)("path", { d: CLOUD }), + done ? (0, import_react5.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react5.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react5.createElement)("path", { d: "M10 9.75v7" }), (0, import_react5.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) - ); - } - var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); -@@ -3086,6 +3086,7 @@ var 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 SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); ++var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); ++var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); ++var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); ++var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); ++var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); ++var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); ++var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); ++var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +var WorkflowIcon = lineIcon(["M4.5 4h4v3.5h-4z", "M11.5 12.5h4V16h-4z", "M6.5 7.5v3.5a2 2 0 0 0 2 2h3"]); - var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); - var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); - var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); -@@ -3093,14 +3093,14 @@ var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 - var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); - var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); - function PinMarkIcon() { -- return (0, import_react3.createElement)( ++var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); ++var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); ++var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); ++var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); ++var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); ++var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); ++function PinMarkIcon() { + return (0, import_react5.createElement)( - "svg", - { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( ++ "svg", ++ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + (0, import_react5.createElement)( - "g", - { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) ++ "g", ++ { transform: "rotate(45 10 10)" }, + (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), + (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) - ) - ); - } -@@ -3112,21 +3112,48 @@ var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h - var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); - var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); - var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); ++ ) ++ ); ++} ++var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); ++var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); ++var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); ++var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); ++var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); ++var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); ++var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); ++var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +var LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]); - var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); - var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); - var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); ++var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); ++var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); ++var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +var ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]); +var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]); +var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]); - var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); - var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; ++var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); ++var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +function FlyBotMark({ size = 24 }) { + return (0, import_react5.createElement)( + "svg", @@ -4145,671 +5638,980 @@ diff --git a/client.js b/client.js + (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }), + (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 }) + ); -+} - - // src/client/questions.js --var import_react4 = require("react"); + } +-@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; ++ ++// src/client/questions.js +var import_react6 = require("react"); - var keyLetter = (index) => String.fromCharCode(65 + index); - var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); - var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; - function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); ++var keyLetter = (index) => String.fromCharCode(65 + index); ++var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); ++var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; ++function useOptionKeys(count, onPick, ownRef) { + const pick2 = (0, import_react6.useRef)(onPick); - pick2.current = onPick; -- (0, import_react4.useEffect)(() => { ++ pick2.current = onPick; + (0, import_react6.useEffect)(() => { - if (count === 0) return void 0; - const onKey = (event) => { - if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -@@ -3146,10 +3146,10 @@ function useOptionKeys(count, onPick, ownRef) { - }, [count, ownRef]); ++ 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)(); +-var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; +-var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; +-function sheetClassNames(sheet) { +- const names = []; +- const visit = (rules) => { +- for (const rule of rules) { +- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); +- if (rule.cssRules) visit(rule.cssRules); ++function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { + const [own, setOwn] = (0, import_react6.useState)(""); + const [picked, setPicked] = (0, import_react6.useState)([]); + const ownRef = (0, import_react6.useRef)(null); + const titleId = (0, import_react6.useId)(); - const options = optionsOf(question); - const multi = question.multiSelect === true; - const custom = own.trim(); -@@ -3167,7 +3167,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - }; - useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); - const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { ++ const options = optionsOf(question); ++ const multi = question.multiSelect === true; ++ const custom = own.trim(); ++ const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; ++ const choose = (label) => { ++ if (multi) { ++ setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); ++ return; ++ } ++ onAnswer({ selected: [label] }); ++ }; ++ const submit = () => { ++ if (!canSubmit) return; ++ onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); ++ }; ++ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); ++ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + const field = ownAnswer ? (0, import_react6.createElement)("textarea", { - ref: ownRef, - rows: 1, - value: own, -@@ -3186,7 +3186,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - }) : null; - const rows = options.map((option, optionIndex) => { - const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( ++ ref: ownRef, ++ rows: 1, ++ value: own, ++ autoComplete: "off", ++ spellCheck: false, ++ className: "bt-q-input", ++ "aria-label": t("Your own answer"), ++ placeholder: t("Type your own answer"), ++ "aria-keyshortcuts": ownLetter?.toLowerCase(), ++ onChange: (event) => setOwn(event.target.value), ++ onKeyDown: (event) => { ++ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; ++ event.preventDefault(); ++ submit(); ++ } ++ }) : null; ++ const rows = options.map((option, optionIndex) => { ++ const selected = multi && picked.includes(option.label); + return (0, import_react6.createElement)( - "button", - { - key: option.label, -@@ -3196,36 +3196,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), - onClick: () => choose(option.label) - }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( ++ "button", ++ { ++ key: option.label, ++ type: "button", ++ className: "bt-q-opt", ++ "aria-pressed": multi ? selected : void 0, ++ "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), ++ onClick: () => choose(option.label) ++ }, + (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), + (0, import_react6.createElement)( - "span", - { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null ++ "span", ++ { className: "bt-q-body" }, + (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label), + option.description ? (0, import_react6.createElement)("span", { className: "bt-q-desc" }, option.description) : null - ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null ++ ), + selected && !raised ? (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) : null - ); - }); - if (raised && field) { -- rows.push((0, import_react4.createElement)( ++ ); ++ }); ++ if (raised && field) { + rows.push((0, import_react6.createElement)( - "label", - { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) ++ "label", ++ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, + ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, field) - )); - } - let foot = null; - if (raised && multi) { -- foot = (0, import_react4.createElement)( ++ )); ++ } ++ let foot = null; ++ if (raised && multi) { + foot = (0, import_react6.createElement)( - "div", - { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) ++ "div", ++ { className: "bt-q-foot" }, + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), + (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) - ); - } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); ++ ); ++ } else if ((raised || multi) && canSubmit) { + foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); - } -- return (0, import_react4.createElement)( ++ } + return (0, import_react6.createElement)( - "form", - { - className: raised ? "bt-q bt-q-raised" : "bt-q", -@@ -3235,24 +3235,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - submit(); - } - }, -- (0, import_react4.createElement)( ++ "form", ++ { ++ className: raised ? "bt-q bt-q-raised" : "bt-q", ++ "aria-labelledby": titleId, ++ onSubmit: (event) => { ++ event.preventDefault(); ++ submit(); ++ } ++ }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-text" }, -- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), -- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, -- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null ++ "div", ++ { className: "bt-q-text" }, + (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), + question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, + raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null - ), -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) ++ ), + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon)) - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( ++ ), + rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, + field && !raised ? (0, import_react6.createElement)( - "div", - { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null ++ "div", ++ { className: "bt-q-custom" }, + (0, import_react6.createElement)("label", { className: "bt-q-field" }, field), + !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null - ) : null, - foot - ); -@@ -3262,29 +3262,29 @@ function SettledQuestion({ question, answer }) { - const prompt = question?.question ?? ""; - const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; - if (chosen.length === 0) { -- return (0, import_react4.createElement)( ++ ) : null, ++ foot ++ ); ++} ++function SettledQuestion({ question, answer }) { ++ const options = optionsOf(question); ++ const prompt = question?.question ?? ""; ++ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; ++ if (chosen.length === 0) { + return (0, import_react6.createElement)( - "div", - { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)), + (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) - ) - ); - } -- return (0, import_react4.createElement)( ++ ) ++ ); ++ } + return (0, import_react6.createElement)( - "div", - { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { ++ "div", ++ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, + prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null, + (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { - const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( ++ const optionIndex = options.findIndex((option) => option.label === label); + return (0, import_react6.createElement)( - "div", - { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) ++ "div", ++ { key: `${label}-${index}`, className: "bt-q-opt" }, + optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)), + (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) - ); - })) - ); -@@ -3292,8 +3292,8 @@ function SettledQuestion({ question, answer }) { - function QuestionCard({ matched }) { - const pending = matched; - const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); ++ ); ++ })) ++ ); ++} ++function QuestionCard({ matched }) { ++ const pending = matched; ++ const questions = pending.questions ?? []; + const [index, setIndex] = (0, import_react6.useState)(0); + const [answers, setAnswers] = (0, import_react6.useState)([]); - const question = questions[index]; - if (question === void 0) return null; - const onAnswer = (answer) => { -@@ -3305,10 +3305,10 @@ function QuestionCard({ matched }) { ++ 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 }); ++ pending.answer({ answers: next }); }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +- try { +- visit(sheet.cssRules); +- } catch { + return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); - } - function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); ++} ++function PendingQuestion({ sessionId, question, actions, inline }) { + const [answered, setAnswered] = (0, import_react6.useState)(null); - if (answered === question.id) return null; - const onAnswer = (answer) => { - setAnswered(question.id); -@@ -3318,10 +3318,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) { - setAnswered(question.id); - void actions.dismissQuestion(sessionId); - }; -- return (0, import_react4.createElement)( ++ if (answered === question.id) return null; ++ const onAnswer = (answer) => { ++ setAnswered(question.id); ++ void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); ++ }; ++ const onDismiss = () => { ++ setAnswered(question.id); ++ void actions.dismissQuestion(sessionId); ++ }; + return (0, import_react6.createElement)( - "div", - { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ++ "div", ++ { className: inline ? "bt-q-inline" : "bt-q-dock" }, + (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) - ); - } - function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -@@ -3329,16 +3329,16 @@ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { - 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 QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const question = useRoster((value) => value.questions?.[sessionId]); ++ const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; ++ const loading = useSession?.(historyLoading) === true; ++ if (question === void 0 || question.callId && (drawn || loading)) return null; + return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); - } - function AnsweredCell({ node }) { - const data = node.data ?? {}; - const questions = Array.isArray(data.questions) ? data.questions : []; - if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; ++} ++function AnsweredCell({ node }) { ++ const data = node.data ?? {}; ++ const questions = Array.isArray(data.questions) ? data.questions : []; ++ if (questions.length === 0) { + return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null; } - const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- return names; ++ const answers = Array.isArray(data.answers) ? data.answers : []; + return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, { - key: question.id, - question, - answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -@@ -3346,37 +3346,37 @@ function AnsweredCell({ node }) { ++ key: question.id, ++ question, ++ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } ++ }))); } - - // src/client/secret-card.js --var import_react5 = require("react"); +-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); ++ ++// src/client/secret-card.js +var import_react7 = require("react"); - var MIN_VALUE = 4; - var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( ++var MIN_VALUE = 4; ++var NONE = {}; +var KeyIcon = () => (0, import_react7.createElement)( - "svg", - { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) ++ "svg", ++ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), + (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) - ); --var LockIcon = () => (0, import_react5.createElement)( ++); +var LockIcon = () => (0, import_react7.createElement)( - "svg", - { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) ++ "svg", ++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), + (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) - ); --var WarnIcon = () => (0, import_react5.createElement)( ++); +var WarnIcon = () => (0, import_react7.createElement)( - "svg", - { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) ++ "svg", ++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), + (0, import_react7.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) - ); - var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); - var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); - var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); ++); ++var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); ++var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); ++var scopeLabel = (scope, roster) => { + if (scope === "all") return t("All my Bots"); - const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); - return names.length === 0 ? t("No Bot") : names.join(t(" and ")); - }; - var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; - function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { -- return (0, import_react5.createElement)("input", { ++ const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); ++ return names.length === 0 ? t("No Bot") : names.join(t(" and ")); ++}; ++var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; ++function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { + return (0, import_react7.createElement)("input", { - className: "bt-secret-input", - type: MASK_BY_CSS ? "text" : "password", - "data-masked": MASK_BY_CSS ? "" : void 0, -@@ -3401,42 +3401,42 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { ++ 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?.(); + } +- return compiled.get(fragment); }); } - function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( +-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(); +- }; ++function ScopeSwitch({ value, onChange, bot }) { + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react7.createElement)( - "div", - { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, -- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -- options.map(([id, text]) => (0, import_react5.createElement)("button", { ++ "div", ++ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, + (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), + options.map(([id, text2]) => (0, import_react7.createElement)("button", { - key: id, - type: "button", - role: "radio", - "aria-checked": value === id, - className: "bt-secret-seg", - onClick: () => onChange(id) -- }, text)) ++ key: id, ++ type: "button", ++ role: "radio", ++ "aria-checked": value === id, ++ className: "bt-secret-seg", ++ onClick: () => onChange(id) + }, text2)) - ); ++ ); } - function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( +- +-// src/client/mark.js +-var import_react2 = require("react"); +-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +-function useGaze(ref, enabled) { +- (0, import_react2.useEffect)(() => { +- if (!enabled) return void 0; +- let frame = 0; +- let point = null; +- const place = () => { +- frame = 0; +- const node = ref.current; +- if (!node || !point) return; +- const box = node.getBoundingClientRect(); +- const clamp = (value) => Math.max(-1, Math.min(1, value)); +- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); +- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); +- }; +- const onMove = (event) => { +- point = { x: event.clientX, y: event.clientY }; +- if (!frame) frame = requestAnimationFrame(place); +- }; +- const onLeave = () => { +- point = null; +- ref.current?.style.setProperty("--bt-gx", "0"); +- ref.current?.style.setProperty("--bt-gy", "0"); +- }; +- window.addEventListener("pointermove", onMove, { passive: true }); +- document.documentElement.addEventListener("pointerleave", onLeave); +- return () => { +- cancelAnimationFrame(frame); +- window.removeEventListener("pointermove", onMove); +- document.documentElement.removeEventListener("pointerleave", onLeave); +- }; +- }, [enabled]); ++function SecretHead({ bot, title, detail, name, onClose }) { + return (0, import_react7.createElement)( - "div", - { className: "bt-q-head bt-secret-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-q-head bt-secret-head" }, + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))), + (0, import_react7.createElement)( - "div", - { className: "bt-q-text" }, -- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), -- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") ++ "div", ++ { className: "bt-q-text" }, + (0, import_react7.createElement)("p", { className: "bt-q-title" }, title), + (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") - ), -- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null ++ ), + onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null - ); ++ ); } - function SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); +-var FIDGETS = ["hop", "sway", "stretch", "look"]; +-function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { +- const look = given ?? lookOf(bot); +- const ref = (0, import_react2.useRef)(null); +- const maskId = (0, import_react2.useRef)(""); +- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; +- const [poked, setPoked] = (0, import_react2.useState)(false); +- useGaze(ref, gaze); +- const common = { +- ref, +- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", +- "data-shape": look.image ? "image" : shapeId(look.shape), +- "data-state": STATES.includes(state) ? state : "idle", +- "data-live": live ? "" : void 0, +- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, +- "data-gaze": gaze ? "" : void 0, +- "data-poke": pokeable ? "" : void 0, +- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, +- role: label ? "img" : void 0, +- "aria-label": label, +- "aria-hidden": label ? void 0 : true, +- onClick: pokeable ? () => setPoked(true) : void 0, +- onAnimationEnd: poked ? (event) => { +- if (/^bt-poke/.test(event.animationName)) setPoked(false); +- } : void 0 ++function SecretCard({ bot, request, roster, actions }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); + const [replacing, setReplacing] = (0, import_react7.useState)(false); + const [busy, setBusy] = (0, import_react7.useState)(false); + const [error, setError] = (0, import_react7.useState)(""); + const noteId = (0, import_react7.useId)(); - const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); - const run = (promise) => { - setBusy(true); -@@ -3452,73 +3452,73 @@ function SecretCard({ bot, request, roster, actions }) { - if (!busy) void run(actions.secretAllow(request.id)); ++ 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 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)( +- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); +- const box = shapeOf(look.shape).size; +- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); ++ const cancel = () => run(actions.secretCancel(request.id)); ++ const long = value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); ++ }; ++ const allow = () => { ++ if (!busy) void run(actions.secretAllow(request.id)); ++ }; ++ const filling = request.kind === "fill" || replacing; + const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; + return (0, import_react7.createElement)( - "div", - { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { ++ "div", ++ { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, + (0, import_react7.createElement)(SecretHead, { - bot, - title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), - detail: request.purpose, - name: request.name, - onClose: busy ? void 0 : cancel - }), -- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, -- filling ? (0, import_react5.createElement)( ++ bot, ++ title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), ++ detail: request.purpose, ++ name: request.name, ++ onClose: busy ? void 0 : cancel ++ }), + request.kind === "allow" ? (0, import_react7.createElement)("div", { className: "bt-secret-saved" }, (0, import_react7.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, + filling ? (0, import_react7.createElement)( - "div", - { className: "bt-secret-body" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), -- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null ++ "div", ++ { className: "bt-secret-body" }, + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), + request.kind === "fill" ? (0, import_react7.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null - ) : null, -- (0, import_react5.createElement)( ++ ) : null, + (0, import_react7.createElement)( - "div", - { className: "bt-secret-note", id: noteId }, - t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), -- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), ++ "div", ++ { className: "bt-secret-note", id: noteId }, ++ t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), + (0, import_react7.createElement)("code", null, `$DSH_SECRET_${request.name}`), - ".", - filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" - ), - warn, -- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, -- (0, import_react5.createElement)( ++ ".", ++ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" ++ ), ++ warn, + error ? (0, import_react7.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, + (0, import_react7.createElement)( - "div", - { className: "bt-q-foot" }, -- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), -- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) ++ "div", ++ { className: "bt-q-foot" }, + request.kind === "allow" && !replacing ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, + (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), + filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) - ) - ); ++ ) ++ ); } - function SettledSecret({ bot, request, roster }) { - const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) ++function SettledSecret({ bot, request, roster }) { ++ const canceled = request.status === "canceled"; + const text2 = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); + return (0, import_react7.createElement)( - "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( ++ "div", + { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text2, "data-status": request.status }, + (0, import_react7.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( ++ "div", ++ { className: "bt-q-head" }, + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 22 })), + (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)( - "p", - { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) ++ "p", ++ { className: "bt-q-title" }, + text2, + canceled ? null : (0, import_react7.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) - )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) ++ )), + canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) - ) ++ ) ); } - function RequestView({ bot, request, roster, actions }) { -- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); +-function AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); ++function RequestView({ bot, request, roster, actions }) { + return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster }); } - function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { - const bot = roster.byId[sessionId]; - if (!bot) return null; - const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); -- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); +-function MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); ++function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { ++ const bot = roster.byId[sessionId]; ++ if (!bot) return null; ++ const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); + if (request) return (0, import_react7.createElement)("div", { className: "bt-q-inline" }, (0, import_react7.createElement)(RequestView, { bot, request, roster, actions })); - if (done && /already saved/.test(contentText(content))) { -- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); ++ if (done && /already saved/.test(contentText(content))) { + return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); - } - return null; ++ } ++ return null; } -@@ -3531,44 +3531,46 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { - 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 useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; +- } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const roster = useRoster((value) => value); ++ const shown = useCards?.((state) => state[sessionId]) ?? NONE; ++ const loading = useSession?.(historyLoading) === true; ++ const bot = roster.byId[sessionId]; ++ if (!bot || currentPart(bot) !== sessionId) return null; ++ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); ++ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); ++ if (waiting.length === 0) return null; + return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); } +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null +var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; - function ScopeEditor({ secret, roster, onSave }) { -- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); -- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); ++function ScopeEditor({ secret, roster, onSave }) { + const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); + const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope); - const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); -- return (0, import_react5.createElement)( ++ const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const next = mode === "all" ? "all" : mode === "none" ? [] : picked; + const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join(); + return (0, import_react7.createElement)( - "div", - { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-edit-block" }, + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), + (0, import_react7.createElement)( - "div", - { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, -- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, + [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text2]) => (0, import_react7.createElement)( - "label", - { key: id, className: "bt-secret-radio" }, -- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), -- text ++ "label", ++ { key: id, className: "bt-secret-radio" }, + (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), + text2 - )) - ), -- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { ++ )) ++ ), + mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.createElement)("button", { - key: bot.id, - type: "button", - className: "bt-secret-bot", - "aria-pressed": picked.includes(bot.id), - onClick: () => toggle(bot.id) -- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, -- (0, import_react5.createElement)( ++ key: bot.id, ++ type: "button", ++ className: "bt-secret-bot", ++ "aria-pressed": picked.includes(bot.id), ++ onClick: () => toggle(bot.id) + }, (0, import_react7.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react7.createElement)(CheckIcon) : null))) : null, + (0, import_react7.createElement)( - "div", - { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) ++ "div", ++ { className: "bt-secret-row-actions" }, + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) - ) ++ ) ); } - function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); +-function clusterSeats(count, frame) { +- if (count === 1) return [[0, 0, frame]]; +- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; +- if (count === 3) { +- const size = frame * 5 / 9; +- const rest = frame - size; +- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; +- } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; +-} +-function RoomAvatar({ room: room2, roster, size = 36 }) { +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const people = members.length === 1 ? [members[0], null] : members; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); +- } +- const seats = clusterSeats(Math.min(people.length, 4), size); +- const overflow = people.length > 4 ? people.length - 3 : 0; +- const gap = size / 14; +- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-cluster", style: { width: size, height: size } }, +- seats.map(([x, y, s], index) => { +- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); +- const style = { left: x, top: y, width: s, height: s }; +- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); +- if (overflow > 0 && index === 3) { +- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); ++function ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); - const long = value.trim().length >= MIN_VALUE; - const save = () => { - if (!long) return; -@@ -3583,66 +3583,66 @@ function ValueEditor({ secret, onSave }) { - setValue(next); - setReplaced(false); - }; -- return (0, import_react5.createElement)( ++ const long = value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (!long) return; ++ void Promise.resolve(onSave(value)).then((ok) => { ++ if (ok) { ++ setValue(""); ++ setReplaced(true); + } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) ++ }); ++ }; ++ const edit = (next) => { ++ setValue(next); ++ setReplaced(false); ++ }; + return (0, import_react7.createElement)( - "div", - { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-edit-block" }, + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), + (0, import_react7.createElement)( - "div", - { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) ++ "div", ++ { className: "bt-secret-inline" }, + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) - ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ++ ), + value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ); } - function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { +- +-// src/client/icons.js +-var import_react3 = require("react"); +-function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++function SecretRow({ secret, roster, actions, run, open, onToggle }) { + const [confirm, setConfirm] = (0, import_react7.useState)(false); + (0, import_react7.useEffect)(() => { - if (!open) setConfirm(false); - }, [open]); - const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); - const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); - const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( ++ if (!open) setConfirm(false); ++ }, [open]); ++ const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); ++ const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); ++ const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); + return (0, import_react7.createElement)( - "div", - { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-item", "data-open": open ? "" : void 0 }, + (0, import_react7.createElement)( - "button", - { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, + (0, import_react7.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta) - ), -- (0, import_react5.createElement)( ++ ), + (0, import_react7.createElement)( - "span", - { className: "bt-set-control" }, -- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) ++ "span", ++ { className: "bt-set-control" }, + secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) - ) - ), -- open ? (0, import_react5.createElement)( ++ ) ++ ), + open ? (0, import_react7.createElement)( - "div", - { className: "bt-secret-edit" }, -- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), -- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-edit" }, + (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), + (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), + (0, import_react7.createElement)( - "div", - { className: "bt-secret-row-actions" }, - confirm ? [ -- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), -- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), -- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) -- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) ++ "div", ++ { className: "bt-secret-row-actions" }, ++ confirm ? [ + (0, import_react7.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), + (0, import_react7.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), + (0, import_react7.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) + ] : (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete")) - ) - ) : null - ); ++ ) ++ ) : null ++ ); } - function AddSecret({ actions, run, onDone }) { -- const [name, setName] = (0, import_react5.useState)(""); -- const [purpose, setPurpose] = (0, import_react5.useState)(""); -- const [value, setValue] = (0, import_react5.useState)(""); +-function 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 AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); + const [value, setValue] = (0, import_react7.useState)(""); - const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; - const save = () => { - if (!ready) return; -@@ -3653,43 +3653,43 @@ function AddSecret({ actions, run, onDone }) { - } - }); - }; -- return (0, import_react5.createElement)( ++ const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (!ready) return; ++ void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { ++ if (ok) { ++ setValue(""); ++ onDone(); ++ } ++ }); ++ }; + return (0, import_react7.createElement)( - "div", - { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-set-row bt-set-paste bt-secret-add" }, + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), + (0, import_react7.createElement)( - "div", - { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) ++ "div", ++ { className: "bt-secret-row-actions" }, + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) - ) ++ ) ); } - function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); +-function ArrowUpIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); ++function SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); - const secrets = roster.secrets ?? []; - return [ -- (0, import_react5.createElement)( ++ const secrets = roster.secrets ?? []; ++ return [ + (0, import_react7.createElement)( - "p", - { key: "about", className: "bt-secret-about" }, - t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), ++ "p", ++ { key: "about", className: "bt-secret-about" }, ++ t("Keys your Bots asked for. Bots never see the values: they use them as "), + (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"), - t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), ++ t(" in their shell, and every value is replaced by "), + (0, import_react7.createElement)("code", null, "[secret:NAME]"), - t(" before anything goes to a model.") - ), -- (0, import_react5.createElement)( ++ t(" before anything goes to a model.") ++ ), + (0, import_react7.createElement)( - Section, - { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null ++ Section, ++ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, + secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, + secrets.map((secret) => (0, import_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), + adding ? (0, import_react7.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null - ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) ++ ), + adding ? null : (0, import_react7.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) - ]; - } - var SECRET_CSS = ` -@@ -3756,6 +3756,144 @@ var SECRET_CSS = ` - @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} - `; - ++ ]; ++} ++var SECRET_CSS = ` ++.bt-secret-head{align-items:center;gap:10px} ++.bt-secret-who{position:relative;flex:none;display:inline-flex} ++.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} ++.bt-secret-lock svg{width:11px;height:11px} ++.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} ++.bt-secret-body{display:flex;flex-direction:column;gap:8px} ++.bt-secret-field{align-items:center;min-height:36px} ++.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} ++.bt-secret-input[data-masked]{-webkit-text-security:disc} ++.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} ++.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} ++.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} ++.bt-secret-glide[data-at="1"]{transform:translateX(100%)} ++.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} ++.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} ++.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} ++/* A key card asks the user to act, so the process group carrying it stays open in every ++ work-details mode (same override as .bt-group-replies in styles.js). */ ++[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} ++[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} ++.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} ++.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} ++@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} ++.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} ++.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} ++.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} ++.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} ++.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} ++.bt-secret-done .bt-q-head{align-items:center} ++.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} ++.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} ++.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} ++.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} ++@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} ++@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} ++.bt-secret-event{display:inline-flex;align-items:center;gap:5px} ++.bt-secret-dock{flex-direction:column;align-items:center} ++.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} ++.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} ++.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} ++.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} ++.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} ++.bt-secret-faces{display:inline-flex} ++.bt-secret-faces>*+*{margin-left:-6px} ++.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} ++.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} ++.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} ++.bt-secret-radios{display:flex;gap:16px;font-size:13px} ++.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} ++.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} ++.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} ++.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} ++.bt-secret-bot svg{width:13px;height:13px} ++.bt-secret-inline{display:flex;gap:8px;align-items:center} ++.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} ++.bt-secret-row-actions .bt-set-hint{flex:1} ++.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} ++.bt-secret-danger svg{width:13px;height:13px} ++.bt-secret-add .bt-input{height:34px} ++@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} ++`; ++ +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -4864,10 +6666,22 @@ diff --git a/client.js b/client.js + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); + } + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; -+} + } +-function MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), +- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) +- ); +function facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; -+} + } +-function WaveIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) + +// src/client/held-reply.js +var import_react8 = require("react"); @@ -4940,70 +6754,123 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) + ), + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null -+ ); + ); +-} +-function ShareIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) + return (0, import_react8.createElement)( + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain -+ ); -+} + ); + } +-function CloseIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); + - // src/client/turn-reasoning.js - function turnReasoning(steps, current) { - const members = steps.length ? steps : [current]; -@@ -3767,7 +3767,7 @@ var thinkingLive = (step) => step.status === "running" && texts(step).length === - 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"); ++// src/client/turn-reasoning.js ++function turnReasoning(steps, current) { ++ const members = steps.length ? steps : [current]; ++ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; ++ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); + } +-var 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" })) ++var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); ++var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); ++var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); ++ ++// src/client/cells.js +var import_react9 = require("react"); - var import_react_dom = require("react-dom"); - var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); - var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); -@@ -3780,29 +3780,29 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, - actions.openSession(target); - }; - const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( ++var import_react_dom = require("react-dom"); ++var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); ++var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); ++function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { ++ const onClick = (event) => { ++ const target = mentionTarget(event); ++ if (target === null) return; ++ event.preventDefault(); ++ event.stopPropagation(); ++ actions.openSession(target); ++ }; ++ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; + return (0, import_react9.createElement)( - "div", - { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -- texts2.map((text, index) => { ++ "div", ++ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, + texts2.map((text2, index) => { - const key = msgKey ? `${msgKey}:${index}` : void 0; - const picked = key ? reactions?.[index] ?? [] : []; - const last = index === texts2.length - 1; - const live = streaming && last; -- return (0, import_react6.createElement)( ++ const key = msgKey ? `${msgKey}:${index}` : void 0; ++ const picked = key ? reactions?.[index] ?? [] : []; ++ const last = index === texts2.length - 1; ++ const live = streaming && last; + return (0, import_react9.createElement)( - "div", - { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( ++ "div", ++ { key: index, className: "bt-msg" }, + (0, import_react9.createElement)( - "div", - { className: "bt-msg-line" }, - lead && last ? lead : null, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-msg-line" }, ++ lead && last ? lead : null, + (0, import_react9.createElement)( - "div", - { className: "bt-bubble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) ++ "div", ++ { className: "bt-bubble" }, + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text2, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) - ), -- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) ++ ), + live ? null : (0, import_react9.createElement)(MessageTools, { text: text2, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) - ), -- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { ++ ), + picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { - key: emoji, - type: "button", - className: "bt-react", -@@ -3813,9 +3813,22 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, - }) ++ key: emoji, ++ type: "button", ++ className: "bt-react", ++ "aria-label": t("Remove reaction {emoji}", { emoji }), ++ onClick: () => actions.toggleReaction(msgKey, index, emoji) ++ }, emoji))) : 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" }) +- ) +- ); +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; - var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; ++var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text2) { + if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; + const match = String(text2 ?? "").trim().match(/^(.+) left(?: the room)?$/); @@ -5015,86 +6882,216 @@ diff --git a/client.js b/client.js + if (name && (who === name || who.endsWith(` (${name})`))) return true; + } + return false; -+} - function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { + } +-var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); +-var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); +-var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); +-var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); +-var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); +-var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); +-var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); +-var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +-var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); +-var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); +-var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +-var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); +-var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +- +-// src/client/questions.js +-var import_react4 = require("react"); +-var keyLetter = (index) => String.fromCharCode(65 + index); +-var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); +-var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; +-function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); +- pick2.current = onPick; +- (0, import_react4.useEffect)(() => { +- if (count === 0) return void 0; ++function useDismiss(open, close, refs) { + (0, import_react9.useEffect)(() => { - if (!open) return void 0; - const onDown = (event) => { - if (refs.some((ref) => ref.current?.contains(event.target))) return; -@@ -3835,12 +3835,12 @@ function useDismiss(open, close, refs) { ++ if (!open) return void 0; ++ const onDown = (event) => { ++ if (refs.some((ref) => ref.current?.contains(event.target))) return; ++ close(); ++ }; + const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ close(); + } }; - }, [open]); +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ }; ++ }, [open]); } --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); +-function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); +- const options = optionsOf(question); +- const multi = question.multiSelect === true; +- const custom = own.trim(); +- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; +- const choose = (label) => { +- if (multi) { +- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react9.useState)(null); + const [place, setPlace] = (0, import_react9.useState)(null); + const [copied, setCopied] = (0, import_react9.useState)(false); + const tools = (0, import_react9.useRef)(null); + const pop = (0, import_react9.useRef)(null); - const close = () => setPanel(null); - useDismiss(panel !== null, close, [tools, pop]); - const open = (kind, event) => { -@@ -3854,36 +3854,36 @@ function MessageTools({ text, name, sessionId, react, actions }) { - setPanel(kind); ++ const close = () => setPanel(null); ++ useDismiss(panel !== null, close, [tools, pop]); ++ const open = (kind, event) => { ++ if (panel === kind) { ++ close(); + return; + } +- onAnswer({ selected: [label] }); ++ const rect = event.currentTarget.getBoundingClientRect(); ++ const below = rect.bottom + 220 < window.innerHeight; ++ setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); ++ setPanel(kind); }; - const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); ++ const copy2 = () => { + void navigator.clipboard?.writeText(text2).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 1e3); - }); ++ setCopied(true); ++ setTimeout(() => setCopied(false), 1e3); ++ }); }; - const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); +- } +- }) : null; +- const rows = options.map((option, optionIndex) => { +- const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( +- "button", +- { +- key: option.label, +- type: "button", +- className: "bt-q-opt", +- "aria-pressed": multi ? selected : void 0, +- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), +- onClick: () => choose(option.label) +- }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( +- "span", +- { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null +- ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( ++ const download2 = () => { + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); - const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); - link.click(); - setTimeout(() => URL.revokeObjectURL(url), 1e3); - }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { ++ const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); ++ link.click(); ++ setTimeout(() => URL.revokeObjectURL(url), 1e3); ++ }; + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { - close(); - run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( ++ close(); ++ run(); + } }, (0, import_react9.createElement)(Icon), label); + return (0, import_react9.createElement)( - "div", - { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( ++ "div", ++ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, + (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( "div", - { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, -- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); +- } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( ++ { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + REACTIONS.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { - close(); - react(emoji); - } }, emoji)) - ), document.body) : null, -- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( ++ close(); ++ react(emoji); ++ } }, emoji)) ++ ), document.body) : null, + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( "div", - { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, - item(t("Copy"), CopyIcon, copy2), -@@ -3891,12 +3891,40 @@ function MessageTools({ text, name, sessionId, react, actions }) { - ), document.body) : null - ); - } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) ++ { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, ++ item(t("Copy"), CopyIcon, copy2), ++ item(t("Download"), DownloadIcon, download2) ++ ), document.body) : null ++ ); ++} +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -5119,31 +7116,42 @@ diff --git a/client.js b/client.js + if (typeof text2 !== "string" || text2.trim() === "") return null; + const peek = peekLine(text2); + return (0, import_react9.createElement)( - "details", - { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) ++ "details", ++ { className: "bt-think" }, + (0, import_react9.createElement)( + "summary", + null, + (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), + t("Thought it through"), + peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null -+ ), + ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( +- "div", +- { className: "bt-q-custom" }, +- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), +- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null +- ) : null, +- foot + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) ); } - function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,19 +3904,33 @@ function useFileMentions({ node, openFile, fileMentions, useTurnData }) { - const location2 = node.location; - const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; - const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { +-function SettledQuestion({ question, answer }) { +- const options = optionsOf(question); +- const prompt = question?.question ?? ""; +- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; +- if (chosen.length === 0) { +- return (0, import_react4.createElement)( ++function useFileMentions({ node, openFile, fileMentions, useTurnData }) { ++ const tail = useTurnData?.("turn-tail"); ++ const location2 = node.location; ++ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; ++ const seq = node.data.finalNode?.seq; + return (0, import_react9.useMemo)(() => { - if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; - return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; - }, [fileMentions, openFile, seq, tail, turn]); - } ++ if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; ++ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; ++ }, [fileMentions, openFile, seq, tail, turn]); ++} +function useClosingText(node, useTurnData) { + const tail = useTurnData?.("turn-tail"); + const seq = node.data.finalNode?.seq; @@ -5155,45 +7163,41 @@ diff --git a/client.js b/client.js + if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; + return (0, import_react9.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react9.createElement)(BotMark, { bot, size: 22 })); +} - function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { - const roster = useRoster((value) => value); - const data = node.data; - const steps = useTurnSteps(String(data.turn)); - const contexts = useTurnContext(String(data.turn)); - const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); ++function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { ++ const roster = useRoster((value) => value); ++ const data = node.data; ++ const steps = useTurnSteps(String(data.turn)); ++ const contexts = useTurnContext(String(data.turn)); ++ const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); + const held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn); + const closing = useClosingText(node, useTurnData); - const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); -- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); ++ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); + const person = typedBy(roster, sessionId, data.turn); + const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text2) => text2.trim() !== ""); - const running = data.status === "running"; - useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); - if (hidden) return null; -@@ -3924,13 +3924,14 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use - const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; - if (texts2.length === 0 && !reasoning && relays.length === 0) return null; - const selfId = roster.byId[sessionId]?.id ?? sessionId; -- return (0, import_react6.createElement)( -- import_react6.Fragment, ++ const running = data.status === "running"; ++ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); ++ if (hidden) return null; ++ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); ++ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; ++ if (texts2.length === 0 && !reasoning && relays.length === 0) return null; ++ const selfId = roster.byId[sessionId]?.id ?? sessionId; + const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null; + return (0, import_react9.createElement)( + import_react9.Fragment, - null, - relays.map((relay) => { - const source = relay.source; - const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { ++ null, ++ relays.map((relay) => { ++ const source = relay.source; ++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + return (0, import_react9.createElement)(EventLine, { - key: relay.seq, - prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), - bots: [sender], -@@ -3940,61 +3940,230 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use - whole: true - }); - }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null ++ key: relay.seq, ++ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), ++ bots: [sender], ++ onOpen: () => { ++ if (sender.id) actions.openExchange(selfId, sender.id); ++ }, ++ whole: true ++ }); ++ }), + reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles + ); @@ -5216,47 +7220,42 @@ diff --git a/client.js b/client.js + { className: "bt-group-msg bt-person-msg", "data-person": name }, + (0, import_react9.createElement)("span", { className: "bt-group-name" }, name), + (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead }) - ); - } - function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { - if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( ++ ); ++} ++function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { ++ if (whole && bots.length === 1) { + return (0, import_react9.createElement)( "div", - { className: "bt-event" }, -- (0, import_react6.createElement)( +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) ++ { className: "bt-event" }, + (0, import_react9.createElement)( - "button", - { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) ++ "button", ++ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, + (0, import_react9.createElement)("span", null, prefix), + (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name) ) ); } - const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- return (0, import_react4.createElement)( ++ const parts = []; + parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix)); - bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( ++ bots.forEach((bot, index) => { + if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner)); + parts.push((0, import_react9.createElement)( - "button", - { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), ++ "button", ++ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), - bot.name - )); - }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); ++ bot.name ++ )); ++ }); + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts); - } --function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { -- const lead = (0, import_react6.createElement)( ++} +var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); +var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); +function isHeldLine(room2, text2, bot) { @@ -5293,12 +7292,10 @@ diff --git a/client.js b/client.js + } + if (held === "held" || isHeldLine(room2, text2, bot)) return null; + const lead = (0, import_react9.createElement)( - "button", - { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) ++ "button", ++ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, + (0, import_react9.createElement)(BotMark, { bot, size: 22 }) - ); -- return (0, import_react6.createElement)( ++ ); + const name = (0, import_react9.createElement)( + "span", + { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, @@ -5307,12 +7304,53 @@ diff --git a/client.js b/client.js + ); + return (0, import_react9.createElement)( "div", - { className: "bt-group-msg" }, -- (0, import_react6.createElement)( +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) ++ { className: "bt-group-msg" }, + name, + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) -+ ); -+} + ); + } +-function QuestionCard({ matched }) { +- const pending = matched; +- const questions = pending.questions ?? []; +- const [index, setIndex] = (0, import_react4.useState)(0); +- const [answers, setAnswers] = (0, import_react4.useState)([]); +- const question = questions[index]; +- if (question === void 0) return null; +- const onAnswer = (answer) => { +- const next = [...answers, { id: question.id, ...answer }]; +- if (index + 1 < questions.length) { +- setAnswers(next); +- setIndex(index + 1); +- return; +- } +- pending.answer({ answers: next }); +- }; +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +-} +-function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); +- if (answered === question.id) return null; +- const onAnswer = (answer) => { +- setAnswered(question.id); +- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); +- }; +- const onDismiss = () => { +- setAnswered(question.id); +- void actions.dismissQuestion(sessionId); +- }; +- return (0, import_react4.createElement)( +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -5369,28 +7407,45 @@ diff --git a/client.js b/client.js + const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) + { className: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react9.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react9.createElement)( - "span", - { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, -- bot.name, -- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null ++ "span", ++ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, + name, + (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle) - ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ++ ), + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null -+ ); -+} + ); + } +-function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { +- const question = useRoster((value) => value.questions?.[sessionId]); +- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; +- const loading = useSession?.(historyLoading) === true; +- if (question === void 0 || question.callId && (drawn || loading)) return null; +- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; + const bot = roster.byId[sessionId]?.name ?? t("your Bot"); + return (0, import_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot }))); -+} + } +-function AnsweredCell({ node }) { +- const data = node.data ?? {}; +- const questions = Array.isArray(data.questions) ? data.questions : []; +- if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; +- } +- const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- key: question.id, +- question, +- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +- }))); +function GlobeMark() { + return (0, import_react9.createElement)( + "svg", @@ -5398,7 +7453,60 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ); -+} + } +- +-// src/client/secret-card.js +-var import_react5 = require("react"); +-var MIN_VALUE = 4; +-var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), +- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) +-); +-var LockIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), +- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) +-); +-var WarnIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), +- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) +-); +-var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); +-var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); +-var scopeLabel = (scope, roster) => { +- if (scope === "all") return t("All Bots"); +- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); +- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); +-}; +-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; +-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +function RemoteFiles({ files, plugin, botId }) { + const [busy, setBusy] = (0, import_react9.useState)(""); + const [error, setError] = (0, import_react9.useState)(""); @@ -5417,7 +7525,8 @@ diff --git a/client.js b/client.js + setError(String(reason?.message ?? reason)); + } finally { + setBusy(""); -+ } + } +- }); + }; + return (0, import_react9.createElement)( + "div", @@ -5428,231 +7537,326 @@ diff --git a/client.js b/client.js + `📎 ${file.name} · ${fileSize(file.size)}` + )), + error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null - ); ++ ); } - function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( +-function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( ++function GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_react9.createElement)( "div", - { className: "bt-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 +- { 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-event" }, + (0, import_react9.createElement)("span", null, prefix), + room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null ); } - function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -@@ -4005,14 +4005,14 @@ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { - 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 SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( ++function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { ++ if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; ++ const self = roster.byId[sessionId]; ++ const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); ++ const other = team || !scope ? void 0 : findBot(scope); ++ const target = other && other.id !== self?.id ? other : self; ++ if (!target) return null; ++ const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); + return (0, import_react9.createElement)( "div", - { className: "bt-event" }, -- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) +- { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") +- ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null ++ { className: "bt-event" }, + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) ); } - function PartLine({ time }) { -- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +-function SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); +- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); +- const run = (promise) => { +- setBusy(true); +- setError(""); +- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); +- }; +- const cancel = () => run(actions.secretCancel(request.id)); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); +- }; +- const allow = () => { +- if (!busy) void run(actions.secretAllow(request.id)); ++function PartLine({ time }) { + return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null); - } - var scrollParent = (node) => { - for (let at = node.parentElement; at; at = at.parentElement) { -@@ -4021,11 +4021,11 @@ var scrollParent = (node) => { - return null; - }; - function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); ++} ++var scrollParent = (node) => { ++ for (let at = node.parentElement; at; at = at.parentElement) { ++ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; ++ } ++ return null; ++}; ++function EarlierParts({ sessionId, roster, actions }) { + const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react9.useRef)(null); + const busy = (0, import_react9.useRef)(false); + const alive = (0, import_react9.useRef)(true); + const restore = (0, import_react9.useRef)(null); - const load = (cursor) => { - if (busy.current) return; - busy.current = true; -@@ -4041,19 +4041,19 @@ function EarlierParts({ sessionId, roster, actions }) { - if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); - }); ++ const load = (cursor) => { ++ if (busy.current) return; ++ busy.current = true; ++ setView((current) => ({ ...current, loading: true, failed: false })); ++ actions.earlier(sessionId, cursor).then((page) => { ++ busy.current = false; ++ if (!alive.current) return; ++ const scroller = top.current && scrollParent(top.current); ++ if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; ++ setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); ++ }, () => { ++ busy.current = false; ++ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); ++ }); }; -- (0, import_react6.useEffect)(() => { +- 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); - return () => { - alive.current = false; - }; - }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { ++ alive.current = true; ++ load(void 0); ++ return () => { ++ alive.current = false; ++ }; ++ }, [sessionId]); + (0, import_react9.useLayoutEffect)(() => { - const pending = restore.current; - restore.current = null; - if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; - }, [view.items]); -- (0, import_react6.useEffect)(() => { ++ const pending = restore.current; ++ restore.current = null; ++ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; ++ }, [view.items]); + (0, import_react9.useEffect)(() => { - const node = top.current; - if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; - const observer = new IntersectionObserver((entries2) => { -@@ -4069,35 +4069,35 @@ function EarlierParts({ sessionId, roster, actions }) { - if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); - else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); - } -- return (0, import_react6.createElement)( ++ const node = top.current; ++ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; ++ const observer = new IntersectionObserver((entries2) => { ++ if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); ++ }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); ++ observer.observe(node); ++ return () => observer.disconnect(); ++ }, [view.cursor, view.loading, view.failed]); ++ const selfId = roster.byId[sessionId]?.id; ++ const rows = []; ++ for (const item of view.items) { ++ const last = rows.at(-1); ++ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); ++ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); ++ } + return (0, import_react9.createElement)( "div", - { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( +- { 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", - { ref: top, className: "bt-earlier-top" }, -- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null +- { className: "bt-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.map((row, index) => { - const key = rows.length - index; - switch (row.kind) { - case "divider": -- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- 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) { ++ case "divider": + return (0, import_react9.createElement)(PartLine, { key, time: row.time }); - case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( ++ case "user": + return (0, import_react9.createElement)("div", { key, className: "bt-urow" }, (0, import_react9.createElement)( - "div", - { className: "bt-ububble" }, -- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, -- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null ++ "div", ++ { className: "bt-ububble" }, + row.text ? (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, + row.images ? (0, import_react9.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null - )); - case "answer": -- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); ++ )); ++ case "answer": + return (0, import_react9.createElement)(AnsweredCell, { key, node: { data: row } }); - case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); ++ case "bot": + return (0, import_react9.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); - case "event": { -- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); ++ case "event": { + if (row.role === "report") return (0, import_react9.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); - const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; - const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); -- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { ++ const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; ++ const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); + return (0, import_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { - if (selfId && other.id) actions.openExchange(selfId, other.id); - }, whole: true }); - } -@@ -4105,7 +4105,7 @@ function EarlierParts({ sessionId, roster, actions }) { - return null; - } - }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) ++ if (selfId && other.id) actions.openExchange(selfId, other.id); ++ }, whole: true }); ++ } ++ default: ++ return null; ++ } ++ }), + (0, import_react9.createElement)(PartLine, { time: view.startedAt }) ); } - function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -@@ -4116,7 +4116,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - const deferred = source.kind === "bot" && source.deferred === true; - const relayed = isRelayContext(data); - const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { +-function SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), +- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( +- "p", +- { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) +- )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) +- ) ++function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { ++ const roster = useRoster((value) => value); ++ const data = node.data; ++ const source = data.source !== null && typeof data.source === "object" ? data.source : {}; ++ const selfId = roster.byId[sessionId]?.id ?? sessionId; ++ const deferred = source.kind === "bot" && source.deferred === true; ++ const relayed = isRelayContext(data); ++ const turn = node.location?.turn?.turn ?? data.turn; + (0, import_react9.useLayoutEffect)(() => { - if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); - }, [relayed, sessionId, turn, source.senderSessionId]); - const order = useSessions((list) => { -@@ -4135,23 +4135,31 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; - }); - const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; ++ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); ++ }, [relayed, sessionId, turn, source.senderSessionId]); ++ const order = useSessions((list) => { ++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; ++ return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; ++ }); ++ const peers = useTurnClock((state) => { ++ if (!relayed) return ""; ++ const turns = state[sessionId] ?? {}; ++ return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const hiddenTurns = roster.exchangeTurns[sessionId]; ++ const unreadOpening = useTurnClock((state) => { ++ const first = openingTurn(state[sessionId], order, hiddenTurns); ++ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++ }); ++ const body = triggerBody(); + return unreadOpening ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; - function triggerBody() { ++ function triggerBody() { + if (source.form === "remote-chat") return null; + if (source.shown === true) return null; + if (source.remote?.handle) { + const message = (0, import_react9.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)(FirstContact, { source, roster, sessionId }), message) : message; + } - if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; ++ if (source.kind === "bot") { + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions }); + if (source.role === "room" || source.role === "kickoff") return null; + if (source.role === "mention") return (0, import_react9.createElement)("div", { className: "bt-event" }, (0, import_react9.createElement)("span", null, contentText(data.content))); - if (source.role === "secret" || deferred) return null; - if (source.role === "post") { - if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; ++ if (source.role === "secret" || deferred) return null; ++ if (source.role === "post") { ++ if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.notice === true || source.senderSessionId === "face-notice") return null; - const room2 = roster.roomsById[sessionId]; - const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); ++ const room2 = roster.roomsById[sessionId]; ++ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); - } - if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); ++ } ++ if (source.role === "report") { + return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); - } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); ++ } + if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); + if (source.role === "retry" || source.retried === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); - const run = relayed ? commRunOf( - order.split(",").filter(Boolean).map(Number), - Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -@@ -4165,7 +4165,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { - if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); - } -- return (0, import_react6.createElement)(EventLine, { ++ const run = relayed ? commRunOf( ++ order.split(",").filter(Boolean).map(Number), ++ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), ++ turn, ++ hiddenTurns ++ ) : null; ++ if (run === "folded") return null; ++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; ++ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); ++ const bots = [sender]; ++ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { ++ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); ++ } + return (0, import_react9.createElement)(EventLine, { - prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), - bots, - onOpen: (bot) => actions.openExchange(selfId, bot.id), -@@ -4174,24 +4174,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - } - if (source.kind === "schedule") { - const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); ++ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), ++ bots, ++ onOpen: (bot) => actions.openExchange(selfId, bot.id), ++ whole: true ++ }); ++ } ++ if (source.kind === "schedule") { ++ const prompts = reminderPrompts(contentText(data.content)); + return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); - } - if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); ++ } ++ if (source.kind === "user-question-reply") return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); - } - } - 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 NewDivider() { + return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New"))); - } - function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { - const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { ++} ++function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { ++ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + (0, import_react9.useEffect)(() => { - if (!open) turnProcess.setOpen(true); - }, [open, turnProcess]); - const turn = node?.location?.turn?.turn ?? node?.data?.turn; - const start = node?.location?.turn?.start?.time; - const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { ++ if (!open) turnProcess.setOpen(true); ++ }, [open, turnProcess]); ++ const turn = node?.location?.turn?.turn ?? node?.data?.turn; ++ const start = node?.location?.turn?.start?.time; ++ const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); + (0, import_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "first", start); - }, [sessionId, turn, start]); - const visit = useUi((state) => state.visits?.[sessionId]); -@@ -4199,11 +4199,67 @@ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTu - const own = state[sessionId]?.[turn]; - return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; - }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; ++ actions.noteTurnTime(sessionId, turn, "first", start); ++ }, [sessionId, turn, start]); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const fresh = useTurnClock((state) => { ++ const own = state[sessionId]?.[turn]; ++ return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; ++ }); + return fresh ? (0, import_react9.createElement)(NewDivider) : null; - } - function Nothing() { - return null; - } ++} ++function Nothing() { ++ return null; ++} +var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]); +var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name); +var SILENT_REGISTRATION_TOOLS = /* @__PURE__ */ new Set([ @@ -5709,13 +7913,35 @@ diff --git a/client.js b/client.js + if (parts.length === 0) return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · ")); +} - var SEPARATOR_GAP_MS = 30 * 60 * 1e3; - function clockLabel(time, now = Date.now()) { - const date = new Date(time); -@@ -4233,10 +4233,19 @@ function openingTurn(turns, firstTurns, hiddenTurns) { - return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; - } - var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; ++var SEPARATOR_GAP_MS = 30 * 60 * 1e3; ++function clockLabel(time, now = Date.now()) { ++ const date = new Date(time); ++ const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); ++ const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); ++ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); ++ if (days === 0) return t("Today {time}", { time: clock4 }); ++ if (days === 1) return t("Yesterday {time}", { time: clock4 }); ++ const sameYear = date.getFullYear() === new Date(now).getFullYear(); ++ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; ++} ++function nextTurnStart(turns, turn) { ++ let next; ++ for (const key in turns) { ++ const candidate = Number(key); ++ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; ++ } ++ return next === void 0 ? void 0 : turns[next].first; ++} ++function openingTurn(turns, firstTurns, hiddenTurns) { ++ if (!turns || !firstTurns) return null; ++ const start = nextTurnStart(turns, -Infinity); ++ if (start === void 0) return null; ++ const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); ++ const listed = firstTurns.split(",").map(Number); ++ const earlier = listed.filter((entry) => entry < turn); ++ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; ++} ++var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +function movedOnSince(visit, turns, hiddenTurns) { + if (typeof visit?.entered !== "number") return false; + for (const key in turns ?? {}) { @@ -5725,105 +7951,204 @@ diff --git a/client.js b/client.js + return false; +} +var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; - function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { - const turn = location2.turn; - const time = location2.end.time; -- (0, import_react6.useEffect)(() => { ++function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { ++ const turn = location2.turn; ++ const time = location2.end.time; + (0, import_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "end", time); - }, [sessionId, turn, time]); - const label = useTurnClock((state) => { -@@ -4250,15 +4250,15 @@ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions - return start !== void 0 && start > visit.seen && start <= visit.entered; - }); - if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, ++ actions.noteTurnTime(sessionId, turn, "end", time); ++ }, [sessionId, turn, time]); ++ const label = useTurnClock((state) => { ++ const start = nextTurnStart(state[sessionId] ?? {}, turn); ++ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const fresh = useTurnClock((state) => { ++ if (typeof visit?.seen !== "number" || time > visit.seen) return false; ++ const start = nextTurnStart(state[sessionId] ?? {}, turn); ++ return start !== void 0 && start > visit.seen && start <= visit.entered; ++ }); ++ if (!label && !fresh) return null; + return (0, import_react9.createElement)( + import_react9.Fragment, - null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++ null, + label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + fresh ? (0, import_react9.createElement)(NewDivider) : null ); } - function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +-function RequestView({ bot, request, roster, actions }) { +- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); ++function Shimmer({ children }) { + return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children); } - var PHRASES = { - thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -@@ -4320,19 +4320,22 @@ function toolActivity(name, arg, findBot, self) { +-function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { +- const bot = roster.byId[sessionId]; +- if (!bot) return null; +- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); +- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); +- if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); ++var PHRASES = { ++ thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], ++ working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] ++}; ++var TOOL_PHRASES = [ ++ [/web_?search|search_?web/i, "Searching online"], ++ [/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) }; + } +- return null; } - function workPhrase(tool) { - if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +-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_react6.useEffect)(() => { ++} ++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, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { ++ if (!key) return void 0; ++ actions.noteLive(sessionId, id, JSON.parse(key)); ++ return () => actions.noteLive(sessionId, id, null); ++ }, [sessionId, id, key]); ++} +function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) { + const [live, setLive] = (0, import_react9.useState)({ replies: [] }); + (0, import_react9.useEffect)(() => { - if (done) return void 0; - let stop = false; - let timer; -@@ -4341,10 +4341,10 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi - const result = await actions.roomProgress(sessionId); - if (stop) return; - const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; ++ if (done) return void 0; ++ let stop = false; ++ let timer; ++ let seen = ""; ++ const tick = async () => { ++ const result = await actions.roomProgress(sessionId); ++ if (stop) return; ++ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; + const key = entry ? JSON.stringify(entry.replies) : ""; - if (entry && key !== seen) { - seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); ++ if (entry && key !== seen) { ++ seen = key; + setLive({ replies: entry.replies ?? [] }); - } - timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); - }; -@@ -4362,71 +4362,89 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi - replies = []; - } - } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; ++ } ++ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); ++ }; ++ void tick(); ++ return () => { ++ stop = true; ++ clearTimeout(timer); ++ }; ++ }, [done, sessionId, callId]); ++ let replies = live.replies; ++ if (done) { ++ try { ++ replies = JSON.parse(contentText(content)).replies ?? []; ++ } catch { ++ replies = []; ++ } ++ } + if (replies.length === 0) return null; - const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ const room2 = roster.roomsById[sessionId]; + return (0, import_react9.createElement)( "div", - { className: "bt-group-replies" }, - replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( +- "label", +- { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text +- )) +- ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { +- key: bot.id, +- type: "button", +- className: "bt-secret-bot", +- "aria-pressed": picked.includes(bot.id), +- onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) +- ) ++ { className: "bt-group-replies" }, ++ replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; + const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored; + const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; - if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( ++ if (reply.kind === "user") { + const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId; + if (reply.other) { + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( - "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); ++ "div", + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( @@ -5832,68 +8157,46 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" }) + ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null -+ ); - } - if (reply.kind === "failed") { -- return (0, import_react6.createElement)( ++ ); ++ } ++ if (reply.kind === "failed") { + return (0, import_react9.createElement)( - "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, ++ "div", + { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status" }, - t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) - ); - } - if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( ++ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) ++ ); ++ } ++ if (reply.kind === "lookup") { + return (0, import_react9.createElement)( - "div", - { key: `${reply.botId}-${index}`, className: "bt-event" }, - reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) - ); - } - if (reply.kind === "missed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); ++ "div", ++ { key: `${reply.botId}-${index}`, className: "bt-event" }, ++ reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) ++ ); ++ } ++ if (reply.kind === "missed") { + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); - } - if (reply.kind === "resumed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); ++ } ++ if (reply.kind === "resumed") { + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); - } - if (reply.kind === "cut") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); ++ } ++ if (reply.kind === "cut") { + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); - } - if (reply.kind === "approval" || reply.kind === "working") { -- return (0, import_react6.createElement)( ++ } ++ if (reply.kind === "approval" || reply.kind === "working") { + return (0, import_react9.createElement)( - "div", - { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( ++ "div", ++ { key: `${reply.botId}-${index}`, className: "bt-event" }, + (0, import_react9.createElement)( - "button", - { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) ++ "button", ++ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) - ) - ); - } - if (reply.kind === "stopped") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); ++ ) ++ ); ++ } ++ if (reply.kind === "stopped") { + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); - } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); -- }), -- pending.map((id) => { -- const bot = roster.byId[id]; -- if (!bot) return null; -- return (0, import_react6.createElement)( -- "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); ++ } + if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null; + if ((reply.kind === "notice" || reply.kind === "left") && leaveNoticeCoveredByClose(room2, reply.text)) return null; + if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") { @@ -5913,154 +8216,224 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react9.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); - }) ++ }) ); } -@@ -4435,52 +4435,52 @@ var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); - function callTree(root) { - const calls = []; - const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; +-function ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!long) return; +- void Promise.resolve(onSave(value)).then((ok) => { +- if (ok) { +- setValue(""); +- setReplaced(true); ++var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); ++var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); ++function callTree(root) { ++ const calls = []; ++ const seen = /* @__PURE__ */ new Set(); + const visit = (call4) => { + const id = call4.callId ?? call4; - if (seen.has(id)) return; - seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); ++ if (seen.has(id)) return; ++ seen.add(id); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); - }; - visit(root); - return calls; - } - function useShownCards(actions, sessionId, callIds) { - const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { ++ }; ++ visit(root); ++ return calls; ++} ++function useShownCards(actions, sessionId, callIds) { ++ const key = JSON.stringify(callIds); + (0, import_react9.useLayoutEffect)(() => { - const ids = JSON.parse(key); - if (ids.length === 0 || !actions.noteCards) return void 0; - actions.noteCards(sessionId, ids, 1); - return () => actions.noteCards(sessionId, ids, -1); - }, [sessionId, key]); - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); ++ const ids = JSON.parse(key); ++ if (ids.length === 0 || !actions.noteCards) return void 0; ++ actions.noteCards(sessionId, ids, 1); ++ return () => actions.noteCards(sessionId, ids, -1); ++ }, [sessionId, key]); ++} +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; + const { text: arg, value: argValue } = toolArgs(call4); - const self = roster.byId[sessionId]; - switch (tool) { - case "message_bot": { - const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); ++ const self = roster.byId[sessionId]; ++ switch (tool) { ++ case "message_bot": { ++ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); - } - case "stop_bot": { - const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); ++ } ++ case "stop_bot": { ++ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); - } - case "create_bot": { - const created = findBot(arg("name")); - if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); ++ } ++ case "create_bot": { ++ const created = findBot(arg("name")); ++ if (!done || created === void 0) return null; + return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); - } - case "update_bot": { - const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; ++ } ++ case "update_bot": { ++ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); + return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; - } - case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); ++ } ++ case "group_relay": + return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi }); - case "create_group": - case "update_group": - case "delete_group": -@@ -4492,64 +4492,90 @@ function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot - post_to_group: ["Posted in", "Couldn't post in the group"] - }[tool]; - if (!done) return null; -- const result = contentText(call2.content); ++ case "create_group": ++ case "update_group": ++ case "delete_group": ++ case "post_to_group": { ++ const verbs = { ++ create_group: ["Created group", "Couldn't create the group"], ++ update_group: ["Updated group", "Couldn't update the group"], ++ delete_group: ["Deleted group", "Couldn't delete the group"], ++ post_to_group: ["Posted in", "Couldn't post in the group"] ++ }[tool]; ++ if (!done) return null; + const result = contentText(call4.content); - const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); ++ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); + if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); - const name = named[2]; - const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); ++ const name = named[2]; ++ const room2 = roster.rooms.find((entry) => entry.name === name); + return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); - } - case "ask_user": { - const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); ++ } ++ case "ask_user": { ++ const question = roster.questions?.[sessionId]; + if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true }); + return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); - } - case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); ++ } ++ case "request_secret": + return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions }); - case "ask_user_question": { - if (!done) return null; - let parsed = null; - try { -- parsed = JSON.parse(contentText(call2.content)); ++ case "ask_user_question": { ++ if (!done) return null; ++ let parsed = null; ++ try { + parsed = JSON.parse(contentText(call4.content)); - } catch { - parsed = null; ++ } catch { ++ parsed = null; } - const answer = parsed?.answers?.[0]; - const question = argValue("questions")?.[0]; - if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- }); +- }; +- const edit = (next) => { +- setValue(next); +- setReplaced(false); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-inline" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) +- ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null +- ); ++ const answer = parsed?.answers?.[0]; ++ const question = argValue("questions")?.[0]; ++ if (!answer) return null; + return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); - } - case "read_group_chat": { - if (!done) return null; - const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; ++ } ++ case "read_group_chat": { ++ if (!done) return null; ++ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); + return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; - } - case "remember": - case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; ++ } ++ case "remember": ++ case "forget": + return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null; - default: - return null; - } ++ default: ++ return null; ++ } } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +-function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) +- ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null +- ); +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { - const roster = useRoster((value) => value); - const root = node.data.root; - const turn = node.location?.turn?.turn ?? root.turn; - const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); ++ const roster = useRoster((value) => value); ++ const root = node.data.root; ++ const turn = node.location?.turn?.turn ?? root.turn; ++ const hidden = isHiddenTurn(roster, sessionId, turn); + const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId); - const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); ++ const calls = hidden ? [] : callTree(root); + const drawn = calls.filter((call4) => DRAWN.has(toolName(call4))); + const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result"); - const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; - useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); ++ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; ++ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); + useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId)); - if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); ++ if (drawn.length === 0) return null; + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); -+} + } +-function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); +- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!ready) return; +- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { +- if (ok) { +- setValue(""); +- onDone(); +- } +- }); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-set-row bt-set-paste bt-secret-add" }, +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) +- ) +- ); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -6068,7 +8441,30 @@ diff --git a/client.js b/client.js + const at = roster?.remoteAwaiting?.[sessionId]; + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; -+} + } +-function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; + +// src/client/remote-approval-wait.js +var REMOTE_APPROVAL_LABEL = "Waiting for their approval"; @@ -6076,8 +8472,76 @@ diff --git a/client.js b/client.js + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; -+} -+ + } +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +-`; + +-// src/client/turn-reasoning.js +-function turnReasoning(steps, current) { +- const members = steps.length ? steps : [current]; +- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; +- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); +// src/client/activity-row-host.js +var RUNNING_HOST = "[data-chat-flow] [data-chat-running]"; +var TAIL_HOST = "tail"; @@ -6087,20 +8551,67 @@ diff --git a/client.js b/client.js + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); } +-var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); +-var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); +-var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); - // src/client/activity-row.js --var import_react7 = require("react"); +-// src/client/cells.js +-var import_react6 = require("react"); +-var import_react_dom = require("react-dom"); +-var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); +-var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); +-function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { +- const onClick = (event) => { +- const target = mentionTarget(event); +- if (target === null) return; +- event.preventDefault(); +- event.stopPropagation(); +- actions.openSession(target); +- }; +- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) +- ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); +- }) +- ); ++// src/client/activity-row.js +var import_react10 = require("react"); - var import_react_dom2 = require("react-dom"); - var PHRASE_HOLD_MS = 800; - var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); -@@ -4559,9 +4559,69 @@ function elapsedLabel(ms) { - const hours = Math.floor(minutes / 60); - return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { ++var import_react_dom2 = require("react-dom"); ++var PHRASE_HOLD_MS = 800; ++var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); ++function elapsedLabel(ms) { ++ const minutes = Math.floor(ms / 6e4); ++ if (minutes < 60) return `${Math.max(1, minutes)}m`; ++ const hours = Math.floor(minutes / 60); ++ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++} +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -6150,59 +8661,128 @@ diff --git a/client.js b/client.js + if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true); + }; + const observer = new MutationObserver(() => { -+ if (!frame) frame = requestAnimationFrame(check); ++ if (!frame) frame = requestAnimationFrame(check); ++ }); ++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); ++ check(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; ++ }, [remote, sending]); ++ return replied; ++} ++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; ++ } ++ let frame = 0; ++ const find = () => { ++ frame = 0; ++ const node = findHost(where); ++ 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 }); -+ check(); ++ find(); + return () => { + cancelAnimationFrame(frame); + observer.disconnect(); + }; -+ }, [remote, sending]); -+ return replied; -+} -+function useRunningHost(active, where = RUNNING_HOST) { -+ const [host, setHost] = (0, import_react10.useState)(null); -+ (0, import_react10.useEffect)(() => { - if (!active) { - setHost(null); - return void 0; -@@ -4569,7 +4569,7 @@ function useRunningHost(active) { - let frame = 0; - const find = () => { - frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -+ const node = findHost(where); - setHost((prev) => prev === node ? prev : node); - }; - const observer = new MutationObserver(() => { -@@ -4581,13 +4581,13 @@ function useRunningHost(active) { - cancelAnimationFrame(frame); - observer.disconnect(); - }; -- }, [active]); + }, [active, where]); - return host; ++ return host; } - function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); ++function useRowEntrances(replacing) { + const check = (0, import_react10.useRef)(replacing); - check.current = replacing; -- (0, import_react7.useEffect)(() => { ++ check.current = replacing; + (0, import_react10.useEffect)(() => { - const seen = /* @__PURE__ */ new WeakSet(); - const started = Date.now(); - let column = null; -@@ -4638,21 +4638,31 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); - const bot = roster.byId[sessionId]; - const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); ++ const seen = /* @__PURE__ */ new WeakSet(); ++ const started = Date.now(); ++ let column = null; ++ let frame = 0; ++ const settle = (event) => { ++ if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; ++ delete event.currentTarget.dataset.btEnter; ++ event.currentTarget.removeEventListener("animationend", settle); + }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); ++ const scan = () => { ++ frame = 0; ++ const next = document.querySelector("[data-chat-flow]"); ++ if (next !== column) { ++ observer.disconnect(); ++ column = next; ++ if (column) observer.observe(column, { childList: true, subtree: true }); ++ } ++ if (!column) return; ++ const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; ++ let last = -1; ++ items.forEach((item, index) => { ++ if (seen.has(item)) last = index; ++ }); ++ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); ++ for (const item of fresh) seen.add(item); ++ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; ++ for (const item of fresh) { ++ if (items.indexOf(item) < last) continue; ++ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); ++ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; ++ item.addEventListener("animationend", settle); + } + }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(scan); ++ }); ++ scan(); + return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); ++ cancelAnimationFrame(frame); ++ observer.disconnect(); + }; +- }, [open]); ++ }, []); + } +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; ++function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { ++ const roster = useRoster((value) => value); ++ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); ++ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); ++ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); ++ const bot = roster.byId[sessionId]; ++ const room2 = roster.roomsById[sessionId]; + const entries2 = (0, import_react10.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); - const streaming = entries2.some((entry) => entry.text); - const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); - const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); ++ const streaming = entries2.some((entry) => entry.text); ++ const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); ++ const relaying = tools.some((entry) => entry.relay); + const awaiting = useAwaitingReply(roster, sessionId); + const sendRun = (0, import_react10.useRef)(false); + if (!running) sendRun.current = false; @@ -6213,49 +8793,59 @@ diff --git a/client.js b/client.js + const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running; + lastRun.current = { running, send: sendRun.current }; + const steady = bridging; - const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); ++ const tool = tools.at(-1); + const inputRef = (0, import_react10.useRef)(0); - const inputAt = Math.max(inputRef.current, pendingInput); - inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); ++ const inputAt = Math.max(inputRef.current, pendingInput); ++ inputRef.current = inputAt; + const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null; + const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; + const [shown, setShown] = (0, import_react10.useState)(null); - const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); ++ const wantKey = want?.key ?? null; + const markState = (0, import_react10.useRef)("thinking"); - if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { ++ if (want) markState.current = want.state; + (0, import_react10.useEffect)(() => { - if (wantKey === null || shown?.key === wantKey) return void 0; - const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); - const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -@@ -4663,26 +4663,31 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const timer = setTimeout(make, wait); - return () => clearTimeout(timer); - }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); ++ if (wantKey === null || shown?.key === wantKey) return void 0; ++ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); ++ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; ++ if (wait <= 0) { ++ make(); ++ return void 0; + } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); +- }); +- }; +- const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); +- link.click(); +- setTimeout(() => URL.revokeObjectURL(url), 1e3); +- }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { +- close(); +- run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( ++ const timer = setTimeout(make, wait); ++ return () => clearTimeout(timer); ++ }, [wantKey, shown]); + const [phase, setPhase] = (0, import_react10.useState)(null); + const endedAt2 = (0, import_react10.useRef)(0); + const shift = (0, import_react10.useRef)(null); + const leaving = (0, import_react10.useRef)(false); - leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { ++ leaving.current = phase !== null && !visible; + (0, import_react10.useEffect)(() => { - if (visible) { - setPhase("in"); - return void 0; - } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); ++ if (visible) { ++ setPhase("in"); ++ return void 0; ++ } + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -6266,35 +8856,29 @@ diff --git a/client.js b/client.js + }; + if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS); + else leave(); - return () => clearTimeout(timer); - }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { ++ return () => clearTimeout(timer); ++ }, [visible]); + (0, import_react10.useLayoutEffect)(() => { - if (phase !== null) return; -- endedAt.current = Date.now(); ++ if (phase !== null) return; + endedAt2.current = Date.now(); - setShown(null); - const before = shift.current; - shift.current = null; -@@ -4691,31 +4691,37 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const delta = before - scroller.scrollTop; - if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); - }, [phase]); -- (0, import_react7.useEffect)(() => { ++ setShown(null); ++ const before = shift.current; ++ shift.current = null; ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ if (before === null || !scroller || reducedMotion()) return; ++ const delta = before - scroller.scrollTop; ++ if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); ++ }, [phase]); + (0, import_react10.useEffect)(() => { - if (!running) actions.clearLive(sessionId); - }, [running, sessionId]); -- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { ++ if (!running) actions.clearLive(sessionId); ++ }, [running, sessionId]); + useRowEntrances(() => leaving.current || Date.now() - endedAt2.current < 300); + const [now, setNow] = (0, import_react10.useState)(Date.now); + (0, import_react10.useEffect)(() => { - if (phase !== "in") return void 0; - const timer = setInterval(() => setNow(Date.now()), 1e4); - return () => clearInterval(timer); - }, [phase]); -- const host = useRunningHost(running); ++ if (phase !== "in") return void 0; ++ const timer = setInterval(() => setNow(Date.now()), 1e4); ++ return () => clearInterval(timer); ++ }, [phase]); + const remote = bot?.remote !== void 0; + const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST); + const drawn = (0, import_react10.useRef)(false); @@ -6302,36 +8886,70 @@ diff --git a/client.js b/client.js + (0, import_react10.useEffect)(() => { + drawn.current = Boolean(host && phase !== null && shown); + }, [host, phase, shown]); - if (!host || phase === null || !shown) return null; - const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; -- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( ++ if (!host || phase === null || !shown) return null; ++ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + return (0, import_react_dom2.createPortal)((0, import_react10.createElement)( "div", -- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, -- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react7.createElement)( +- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, +- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { +- close(); +- react(emoji); +- } }, emoji)) +- ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, +- item(t("Copy"), CopyIcon, copy2), +- item(t("Download"), DownloadIcon, download2) +- ), document.body) : null +- ); + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react10.createElement)( - "span", - { className: "bt-typing-text" }, -- (0, import_react7.createElement)( ++ "span", ++ { className: "bt-typing-text" }, + (0, import_react10.createElement)( - "span", - { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, -- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), -- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null ++ "span", ++ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null - ) - ) - ), host); -@@ -4731,49 +4731,1896 @@ function showsDshRow(dsh, app) { - return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ ) ++ ) ++ ), host); } - --// src/client/sidebar.js --var import_react8 = require("react"); +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +- "details", +- { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +- ); ++ ++// src/client/dsh-offer.js ++var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); ++var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; ++var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); ++function showsDshRow(dsh, app) { ++ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; ++ if (dsh.kind === "npm") return true; ++ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); + } +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; +- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; +- }, [fileMentions, openFile, seq, tail, turn]); ++ +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; @@ -6349,7 +8967,41 @@ diff --git a/client.js b/client.js + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); + }; + }, []); -+} + } +-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- const running = data.status === "running"; +- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); +- if (hidden) return null; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; +- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true +- }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +- ); +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -6384,7 +9036,31 @@ diff --git a/client.js b/client.js +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); -+} + } +-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { +- if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, +- (0, import_react6.createElement)("span", null, prefix), +- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) +- ) +- ); +- } +- const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( +- "button", +- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- bot.name +- )); +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); @@ -6395,8 +9071,11 @@ diff --git a/client.js b/client.js + 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 }] : []; -+ }); -+} + }); +- 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)( +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -6418,12 +9097,18 @@ diff --git a/client.js b/client.js + const text2 = `${label} · ${workText(tool, step)}`; + const elapsed = elapsedSince(now, at); + return (0, import_react12.createElement)( -+ "button", + "button", +- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) + { type: "button", className: "bt-mw-row", "aria-label": [text2, 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, text2)), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -+ ); + ); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-group-msg" }, +- (0, import_react6.createElement)( +} +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); @@ -6433,15 +9118,26 @@ diff --git a/client.js b/client.js + { type: "button", className: "bt-mw-row bt-mw-line", "aria-label": [waiting, doing, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, + (0, import_react12.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react12.createElement)(Face, { key: entry.id, entry, size: 16 }))), + (0, import_react12.createElement)( -+ "span", + "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 + { className: "bt-mw-row-text" }, + waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, + waiting && doing ? " · " : null, + doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null -+ ), + ), +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -+ ); -+} + ); + } +-function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null +- ); +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -6475,7 +9171,16 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); -+} + } +-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- const target = other && other.id !== self?.id ? other : self; +- if (!target) return null; +- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( +function useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -6514,7 +9219,9 @@ diff --git a/client.js b/client.js + void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); + }; + return (0, import_react12.createElement)( -+ "div", + "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-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react12.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react12.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, @@ -6531,8 +9238,14 @@ diff --git a/client.js b/client.js + { className: "bt-mw-actions" }, + (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 PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +-} +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -6542,17 +9255,99 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), + entry.approvals.map((approval) => (0, import_react12.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); -+ } + } +- return null; +-}; +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +- }); + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); + void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); -+ }; + }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); +- return () => { +- alive.current = false; +- }; +- }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { +- const pending = restore.current; +- restore.current = null; +- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; +- }, [view.items]); +- (0, import_react6.useEffect)(() => { +- const node = top.current; +- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; +- const observer = new IntersectionObserver((entries2) => { +- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); +- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); +- observer.observe(node); +- return () => observer.disconnect(); +- }, [view.cursor, view.loading, view.failed]); +- const selfId = roster.byId[sessionId]?.id; +- const rows = []; +- for (const item of view.items) { +- const last = rows.at(-1); +- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); +- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); +- } +- return (0, import_react6.createElement)( + return (0, import_react12.createElement)( -+ "div", + "div", +- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( + { className: "bt-mw-member" }, + (0, import_react12.createElement)( -+ "div", + "div", +- { ref: top, className: "bt-earlier-top" }, +- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null +- ), +- rows.map((row, index) => { +- const key = rows.length - index; +- switch (row.kind) { +- case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null +- )); +- case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); +- case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); +- case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); +- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; +- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { +- if (selfId && other.id) actions.openExchange(selfId, other.id); +- }, whole: true }); +- } +- default: +- return null; + { className: "bt-mw-state bt-mw-work" }, + (0, import_react12.createElement)("span", null, (0, import_react12.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null @@ -6737,11 +9532,39 @@ diff --git a/client.js b/client.js + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; -+ } + } +- }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) +- ); + }; + }, []); + return workflows; -+} + } +-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- const deferred = source.kind === "bot" && source.deferred === true; +- const relayed = isRelayContext(data); +- const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { +- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); +- }, [relayed, sessionId, turn, source.senderSessionId]); +- const order = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; +- }); +- const peers = useTurnClock((state) => { +- if (!relayed) return ""; +- const turns = state[sessionId] ?? {}; +- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const hiddenTurns = roster.exchangeTurns[sessionId]; +- const unreadOpening = useTurnClock((state) => { +- const first = openingTurn(state[sessionId], order, hiddenTurns); +- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -6844,7 +9667,44 @@ diff --git a/client.js b/client.js + title: count === 1 ? t("1 message waits for your approval") : t("{count} messages wait for your approval", { count }), + hint: t("Approve it in the chat"), + ...extra -+ }); + }); +- const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; +- function triggerBody() { +- if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; +- if (source.role === "secret" || deferred) return null; +- if (source.role === "post") { +- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; +- const room2 = roster.roomsById[sessionId]; +- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); +- } +- if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); +- } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); +- const run = relayed ? commRunOf( +- order.split(",").filter(Boolean).map(Number), +- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +- turn, +- hiddenTurns +- ) : null; +- if (run === "folded") return null; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); +- const bots = [sender]; +- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { +- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); +- } +- return (0, import_react6.createElement)(EventLine, { +- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), +- bots, +- onOpen: (bot) => actions.openExchange(selfId, bot.id), +- whole: true +- }); + for (const bot of roster.bots ?? []) { + const count = bot.pendingCount ?? 0; + if (count <= 0) continue; @@ -6923,7 +9783,10 @@ diff --git a/client.js b/client.js + const column = columnOf(task); + if (column === "in-progress") progress.push({ ...card, status: "task", since: startedAtOf(task) }); + else if (column === "done" && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "cancelled" ? "cancelled" : "done" }); -+ } + } +- 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")); + } + for (const graph of Array.isArray(workflows) ? workflows : []) { + if (graph === null || typeof graph !== "object") continue; @@ -6935,7 +9798,9 @@ diff --git a/client.js b/client.js + const at2 = endedAt(run); + if (!fresh(at2)) continue; + if (latest === void 0 || at2 > endedAt(latest)) latest = run; -+ } + } +- if (source.kind === "user-question-reply") return null; +- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); + if (latest === void 0) continue; + const at = endedAt(latest); + const name = typeof graph.name === "string" && graph.name.trim() !== "" ? graph.name.trim() : String(graph.id ?? t("A workflow")); @@ -7000,12 +9865,14 @@ diff --git a/client.js b/client.js + const note = choice !== null && typeof choice === "object" ? String(choice.note ?? "").trim() : ""; + if (note === "") return void 0; + return Promise.resolve(actions.sendBackTask(card.task.id, note)); -+ } + } + if (card?.kind === "approval" && card.approval?.answerable === true) { + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 0; -+} + } +-function NewDivider() { +- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -7059,7 +9926,24 @@ diff --git a/client.js b/client.js + } }, t("Send back")), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "accept") }, t("Accept")) + ); -+} + } +-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- const own = state[sessionId]?.[turn]; +- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; +- }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; +function AnswerFields({ card, busy, onAnswer }) { + const [, redraw] = (0, import_react13.useState)(0); + const question = card.kind === "question"; @@ -7119,7 +10003,9 @@ diff --git a/client.js b/client.js + }), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); -+} + } +-function Nothing() { +- return null; +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { + const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; @@ -7143,15 +10029,31 @@ diff --git a/client.js b/client.js + wide ? (0, import_react13.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null + ); -+} + } +-var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +-function clockLabel(time, now = Date.now()) { +- const date = new Date(time); +- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); +- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); +- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); +- if (days === 0) return t("Today {time}", { time: clock4 }); +- if (days === 1) return t("Yesterday {time}", { time: clock4 }); +- const sameYear = date.getFullYear() === new Date(now).getFullYear(); +- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +function Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; + return (0, import_react13.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", null, t(label))); -+} + } +-function nextTurnStart(turns, turn) { +- let next; +- for (const key in turns) { +- const candidate = Number(key); +- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +function whenText(card, now) { + if (card.status === "running" || card.status === "task") { + return typeof card.since === "number" && now - card.since >= 6e4 ? t("for {time}", { time: elapsedLabel(now - card.since) }) : t("just started"); -+ } + } +- return next === void 0 ? void 0 : turns[next].first; + return typeof card.at === "number" ? relativeShort(card.at) : ""; +} +function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) { @@ -7213,7 +10115,15 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) + ); -+} + } +-function openingTurn(turns, firstTurns, hiddenTurns) { +- if (!turns || !firstTurns) return null; +- const start = nextTurnStart(turns, -Infinity); +- if (start === void 0) return null; +- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); +- const listed = firstTurns.split(",").map(Number); +- const earlier = listed.filter((entry) => entry < turn); +- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; +function useNow2(ms) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -7221,7 +10131,30 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [ms]); + return now; -+} + } +-var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { +- const turn = location2.turn; +- const time = location2.end.time; +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "end", time); +- }, [sessionId, turn, time]); +- const label = useTurnClock((state) => { +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- if (typeof visit?.seen !== "number" || time > visit.seen) return false; +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start > visit.seen && start <= visit.entered; +- }); +- if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null +function openCard(actions, card) { + actions.closeInbox(); + if (card?.kind === "workflow") { @@ -7305,8 +10238,10 @@ diff --git a/client.js b/client.js + )) + ) + ) -+ ); -+} + ); + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +var INBOX_CSS = ` +.bt-inbox-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-inbox-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -7371,7 +10306,10 @@ diff --git a/client.js b/client.js + id: String(task.id), + title: String(task.title ?? "") + }; -+} + } +-var PHRASES = { +- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] + +// src/client/board-card.js +var import_react14 = require("react"); @@ -7385,7 +10323,42 @@ diff --git a/client.js b/client.js + label: "bt-board-label", + owner: "bt-board-who", + note: "bt-board-note" -+}; + }; +-var TOOL_PHRASES = [ +- [/web_?search|search_?web/i, "Searching online"], +- [/fetch|browse|browser|web/i, "Reading a page"], +- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], +- [/^(read|view|cat)\b|read_/i, "Opening a file"], +- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], +- [/write|edit|patch|apply|replace/i, "Writing changes"], +- [/image|photo|draw/i, "Working on an image"] +-]; +-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; +-function toolActivity(name, arg, findBot, self) { +- switch (name) { +- case "ask_user": +- return null; +- case "request_secret": +- return null; +- case "list_secrets": +- return { label: t("Checking the keys"), state: "searching" }; +- case "ask_user_question": +- return { label: t("Waiting for your answer"), state: "alert" }; +- case "group_relay": +- return { relay: true, state: "orbit" }; +- case "message_bot": { +- const target = findBot(arg("to")); +- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; +- } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +- } +- case "create_bot": +- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; +function BoardCard({ + card, + skin = TASK_SKIN, @@ -7513,7 +10486,19 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); -+ } + } +- case "list_bots": +- return { label: t("Checking the team"), state: "searching" }; +- case "read_group_chat": +- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; +- case "read_own_chat": +- return { label: t("Looking back through the chat"), state: "searching" }; +- case "remember": +- return { label: t("Saving to memory"), state: "working" }; +- case "forget": +- return { label: t("Updating memory"), state: "working" }; +- case "recall": +- return { label: t("Checking memory"), state: "searching" }; + }; + 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 })); @@ -7547,10 +10532,42 @@ diff --git a/client.js b/client.js + return t("Cancelled"); + case "failed": + return t("Failed"); -+ default: + default: +- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; +- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; +- return { label: phrase ? t(phrase) : null, state: toolState(name) }; + return t("Not started"); -+ } -+} + } + } +-function workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +-} +-function useLiveNote(actions, sessionId, id, entry) { +- const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { +- if (!key) return void 0; +- actions.noteLive(sessionId, id, JSON.parse(key)); +- return () => actions.noteLive(sessionId, id, null); +- }, [sessionId, id, key]); +-} +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +- if (done) return void 0; +- let stop = false; +- let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +- } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -7599,13 +10616,19 @@ diff --git a/client.js b/client.js + if (stopped || !Array.isArray(list)) return; + setArchive(list.filter((task) => columnOf(task) === "done")); + }).catch(() => void 0); -+ }; + }; +- void tick(); + load(); + const timer = setInterval(load, POLL_MS3); -+ return () => { + return () => { +- stop = true; +- clearTimeout(timer); + stopped = true; + clearInterval(timer); -+ }; + }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { + }, [key]); + const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + const replace = (task) => { @@ -7848,11 +10871,20 @@ diff --git a/client.js b/client.js + const task = tasks.find((item) => item.id === id) ?? own.find((item) => item.id === id); + if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return; + setError(""); -+ try { + try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; + replace(await actions.taskUpdate({ id, column })); + } catch (failure) { + setError(failure?.message ?? String(failure)); -+ } + } +- } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; +- const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( + }; + const create = async (title) => { + const text2 = String(title ?? "").trim(); @@ -7867,14 +10899,73 @@ diff --git a/client.js b/client.js + }; + const name = scopeName(roster, scope); + return (0, import_react15.createElement)( -+ "div", + "div", +- { className: "bt-group-replies" }, +- replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); +- } +- if (reply.kind === "failed") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, +- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) +- ); +- } +- if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) +- ); +- } +- if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); +- } +- if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); +- } +- if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); +- } +- if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) +- ) +- ); +- } +- if (reply.kind === "stopped") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); +- } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); +- }), +- pending.map((id) => { +- const bot = roster.byId[id]; +- if (!bot) return null; +- return (0, import_react6.createElement)( + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react15.createElement)( + "div", + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react15.createElement)( -+ "div", + "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); +- }) + { className: "bt-mem-head" }, + (0, import_react15.createElement)("h2", { id: "bt-board-heading" }, t("Board")), + (0, import_react15.createElement)("span", { className: "bt-mem-status" }, name), @@ -7899,8 +10990,22 @@ diff --git a/client.js b/client.js + onCreate: create + }) + ) -+ ); -+} + ); + } +-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +-function callTree(root) { +- const calls = []; +- const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; +- if (seen.has(id)) return; +- seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); +- }; +- visit(root); +- return calls; +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)} @@ -7962,7 +11067,15 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} -+} + } +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +`; + +// src/client/role-labels.js @@ -7970,11 +11083,86 @@ diff --git a/client.js b/client.js +function roleText(role) { + if (role === null || role === void 0 || role === "") return ""; + return t(ROLE_LABELS[role] ?? String(role)); -+} + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +- const self = roster.byId[sessionId]; +- switch (tool) { +- case "message_bot": { +- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "stop_bot": { +- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "create_bot": { +- const created = findBot(arg("name")); +- if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); +- } +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; +- } +- case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); +- case "create_group": +- case "update_group": +- case "delete_group": +- case "post_to_group": { +- const verbs = { +- create_group: ["Created group", "Couldn't create the group"], +- update_group: ["Updated group", "Couldn't update the group"], +- delete_group: ["Deleted group", "Couldn't delete the group"], +- post_to_group: ["Posted in", "Couldn't post in the group"] +- }[tool]; +- if (!done) return null; +- const result = contentText(call2.content); +- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); +- const name = named[2]; +- const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); +- } +- case "ask_user": { +- const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- } +- case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); +- case "ask_user_question": { +- if (!done) return null; +- let parsed = null; +- try { +- parsed = JSON.parse(contentText(call2.content)); +- } catch { +- parsed = null; +- } +- const answer = parsed?.answers?.[0]; +- const question = argValue("questions")?.[0]; +- if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- } +- case "read_group_chat": { +- if (!done) return null; +- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +- } +- case "remember": +- case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; +- default: +- return null; + +// src/client/projects-nav.js +var import_react16 = require("react"); - var import_react_dom3 = require("react-dom"); ++var import_react_dom3 = require("react-dom"); +var PROJECTS_NAV_CSS = ` +.bt-projects-row { display: flex; align-items: center; gap: 8px; } +.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; } @@ -8007,14 +11195,40 @@ diff --git a/client.js b/client.js + for (const role of choices) { + if (role === current) continue; + items.push({ role, label: roleText(role), current: false, pick: true }); -+ } + } + return items; -+} + } +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +- const roster = useRoster((value) => value); +- const root = node.data.root; +- const turn = node.location?.turn?.turn ?? root.turn; +- const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); +- const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); +- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; +- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); +- if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +function roleNote(answer) { + if (answer?.changed === true) return t("Role changed."); + const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused"); + return t("Role unchanged: {reason}", { reason }); -+} + } +- +-// src/client/activity-row.js +-var import_react7 = require("react"); +-var import_react_dom2 = require("react-dom"); +-var PHRASE_HOLD_MS = 800; +-var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)( + "button", @@ -8040,14 +11254,50 @@ diff --git a/client.js b/client.js + rows, + note === "" || note === void 0 ? null : (0, import_react16.createElement)("div", { className: "bt-projects-note", role: "status" }, note) + ); -+} + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +- } +- let frame = 0; +- const find = () => { +- frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); +- setHost((prev) => prev === node ? prev : node); +- }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(find); +- }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); +- return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); +- }; +- }, [active]); +- return host; +function Chevron() { + return (0, import_react16.createElement)( + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react16.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }) + ); -+} + } +-function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); +- check.current = replacing; +- (0, import_react7.useEffect)(() => { +- const seen = /* @__PURE__ */ new WeakSet(); +- const started = Date.now(); +- let column = null; +- let frame = 0; +- const settle = (event) => { +- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; +- delete event.currentTarget.dataset.btEnter; +- event.currentTarget.removeEventListener("animationend", settle); +function ProjectsMark() { + return (0, import_react16.createElement)( + "svg", @@ -8061,16 +11311,75 @@ diff --git a/client.js b/client.js + (0, import_react16.useEffect)(() => { + const onDown = (event) => { + if (!ref.current?.contains(event.target)) onClose(); -+ }; + }; +- const scan = () => { +- frame = 0; +- const next = document.querySelector("[data-chat-flow]"); +- if (next !== column) { +- observer.disconnect(); +- column = next; +- if (column) observer.observe(column, { childList: true, subtree: true }); +- } +- if (!column) return; +- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; +- let last = -1; +- items.forEach((item, index) => { +- if (seen.has(item)) last = index; +- }); +- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); +- for (const item of fresh) seen.add(item); +- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; +- for (const item of fresh) { +- if (items.indexOf(item) < last) continue; +- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); +- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; +- item.addEventListener("animationend", settle); +- } + const onKey = (event) => { + if (event.key === "Escape") onClose(); -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); +- }); +- scan(); + window.addEventListener("mousedown", onDown, true); + window.addEventListener("keydown", onKey, true); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + window.removeEventListener("mousedown", onDown, true); + window.removeEventListener("keydown", onKey, true); -+ }; + }; +- }, []); +-} +-function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { +- const roster = useRoster((value) => value); +- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); +- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); +- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); +- const bot = roster.byId[sessionId]; +- const room2 = roster.roomsById[sessionId]; +- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); +- const streaming = entries2.some((entry) => entry.text); +- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); +- const relaying = tools.some((entry) => entry.relay); +- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); +- const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); +- const inputAt = Math.max(inputRef.current, pendingInput); +- inputRef.current = inputAt; +- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; +- const [shown, setShown] = (0, import_react7.useState)(null); +- const wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); +- if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { +- if (wantKey === null || shown?.key === wantKey) return void 0; +- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); +- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; +- if (wait <= 0) { +- make(); +- return void 0; + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react16.createElement)( @@ -8104,7 +11413,19 @@ diff --git a/client.js b/client.js + if (onOpen) { + onOpen(id); + return; -+ } + } +- const timer = setTimeout(make, wait); +- return () => clearTimeout(timer); +- }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); +- leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { +- if (visible) { +- setPhase("in"); +- return void 0; + if (typeof window !== "undefined") window.location.hash = projectHash(id); + }, [onOpen]); + const [expanded, setExpanded] = (0, import_react16.useState)(false); @@ -8121,7 +11442,39 @@ diff --git a/client.js b/client.js + } catch (failure) { + setProjects([]); + setError(failure?.message ?? String(failure)); -+ } + } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); +- return () => clearTimeout(timer); +- }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { +- if (phase !== null) return; +- endedAt.current = Date.now(); +- setShown(null); +- const before = shift.current; +- shift.current = null; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (before === null || !scroller || reducedMotion()) return; +- const delta = before - scroller.scrollTop; +- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); +- }, [phase]); +- (0, import_react7.useEffect)(() => { +- if (!running) actions.clearLive(sessionId); +- }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { +- if (phase !== "in") return void 0; +- const timer = setInterval(() => setNow(Date.now()), 1e4); +- return () => clearInterval(timer); +- }, [phase]); +- const host = useRunningHost(running); +- if (!host || phase === null || !shown) return null; +- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( + }, [read]); + (0, import_react16.useEffect)(() => { + if (!expanded) return; @@ -8137,7 +11490,30 @@ diff --git a/client.js b/client.js + }; + const listed = expanded && wide; + return (0, import_react16.createElement)( -+ "div", + "div", +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( +- "span", +- { className: "bt-typing-text" }, +- (0, import_react7.createElement)( +- "span", +- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null +- ) +- ) +- ), host); +-} +- +-// src/client/dsh-offer.js +-var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +-var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; +-var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); +-function showsDshRow(dsh, app) { +- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; +- if (dsh.kind === "npm") return true; +- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); + { className: "bt-projects" }, + (0, import_react16.createElement)( + "button", @@ -8172,9 +11548,11 @@ diff --git a/client.js b/client.js + } + }) + ); -+} -+ -+// src/client/sidebar.js + } + + // src/client/sidebar.js +-var import_react8 = require("react"); +-var import_react_dom3 = require("react-dom"); +var import_react17 = require("react"); +var import_react_dom4 = require("react-dom"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -8384,7 +11762,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, +@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -8446,7 +11824,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -8459,7 +11837,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -8501,7 +11879,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -8589,7 +11967,7 @@ diff --git a/client.js b/client.js useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); useLandOnMainBot(roster, useSessions, actions); const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -@@ -5045,39 +5045,43 @@ function TeamSidebar(props) { +@@ -5045,42 +5045,258 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; @@ -8650,163 +12028,151 @@ diff --git a/client.js b/client.js + const [dshError, setDshError] = (0, import_react17.useState)(""); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - if (!open) return void 0; - const onDown = (event) => { - if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) { - setDshError(""); - void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); - }; -- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); ++ if (!open) return void 0; ++ const onDown = (event) => { ++ if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; ++ setOpen(false); ++ }; ++ const onKey = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ setOpen(false); ++ button.current?.focus(); ++ } ++ }; ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ }; ++ }, [open]); ++ const phase = update?.phase ?? "idle"; ++ const dsh = update?.dsh; ++ const dshPhase = dsh?.phase ?? "idle"; ++ const dshRow = showsDshRow(dsh, update?.app); ++ if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; ++ const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; ++ const toggle = () => { ++ const rect = button.current?.getBoundingClientRect(); ++ if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); ++ setOpen((value) => !value); ++ }; ++ const start = () => { ++ setError(""); ++ void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); ++ }; ++ const startDsh = () => { ++ setDshError(""); ++ void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); ++ }; + const label = phase === "installing" ? t("Updating FlyBot core…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("FlyBot core {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("FlyBot core {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); - const message = error || (phase === "failed" ? update.error : ""); - const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); -- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); -- return (0, import_react8.createElement)( -- import_react8.Fragment, ++ const message = error || (phase === "failed" ? update.error : ""); ++ const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); + const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); + return (0, import_react17.createElement)( + import_react17.Fragment, - null, -- (0, import_react8.createElement)("button", { ++ null, + (0, import_react17.createElement)("button", { - ref: button, - type: "button", - className: "bt-round bt-update", -@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) { - "aria-haspopup": "dialog", - "aria-expanded": open, - onClick: toggle -- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ref: button, ++ type: "button", ++ className: "bt-round bt-update", ++ "data-phase": cloudPhase, ++ "aria-label": label, ++ title: label, ++ "aria-haspopup": "dialog", ++ "aria-expanded": open, ++ onClick: toggle + }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)( - "div", -- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, -- update?.available || phase !== "idle" ? (0, import_react8.createElement)( -- import_react8.Fragment, ++ "div", + { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box }, + update?.available || phase !== "idle" ? (0, import_react17.createElement)( + import_react17.Fragment, - null, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), -- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -- (0, import_react8.createElement)( ++ null, + (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), + message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react17.createElement)( - "div", - { className: "bt-update-actions" }, -- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -- phase === "installed" ? null : (0, import_react8.createElement)("button", { ++ "div", ++ { className: "bt-update-actions" }, + update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react17.createElement)("button", { - type: "button", - className: "bt-update-go", - disabled: phase === "installing", -@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) { - }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) - ) - ) : null, -- dshRow ? (0, import_react8.createElement)( ++ type: "button", ++ className: "bt-update-go", ++ disabled: phase === "installing", ++ onClick: start ++ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ++ ) ++ ) : null, + dshRow ? (0, import_react17.createElement)( - "div", - { className: "bt-update-dsh" }, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), -- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -- (0, import_react8.createElement)( ++ "div", ++ { className: "bt-update-dsh" }, + (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText), + dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, + (0, import_react17.createElement)( - "div", - { className: "bt-update-actions" }, -- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { -- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { ++ "div", ++ { className: "bt-update-actions" }, + dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { + }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", { - type: "button", - className: "bt-update-go", - disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { - ), document.body) : null - ); - } --function ConnectPlugins({ wide, actions }) { -- const tipId = (0, import_react8.useId)(); -- const [tip, setTip] = (0, import_react8.useState)(false); -- const timer = (0, import_react8.useRef)(0); -- const button = (0, import_react8.useRef)(null); -- const [box, setBox] = (0, import_react8.useState)(null); -- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); ++ type: "button", ++ className: "bt-update-go", ++ disabled: dshPhase === "installing", ++ onClick: startDsh ++ }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) ++ ) ++ ) : null ++ ), document.body) : null ++ ); ++} +function ConnectApps({ wide, actions }) { - if (!wide) return null; -- const show = () => { -- clearTimeout(timer.current); -- timer.current = setTimeout(() => { -- const rect = button.current?.getBoundingClientRect(); -- if (!rect) return; -- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); -- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); -- setTip(true); -- }, 250); -- }; -- const hide = () => { -- clearTimeout(timer.current); -- setTip(false); -- }; -- return (0, import_react8.createElement)( ++ if (!wide) return null; + return (0, import_react17.createElement)( - "span", - { className: "bt-connect-wrap" }, -- (0, import_react8.createElement)( ++ "span", ++ { className: "bt-connect-wrap" }, + (0, import_react17.createElement)( - "button", -- { -- ref: button, -- type: "button", -- className: "bt-connect bt-connect-foot", -- "aria-describedby": tip ? tipId : void 0, -- onMouseEnter: show, -- onMouseLeave: hide, -- onFocus: show, -- onBlur: hide, -- onClick: () => actions.selectPanel("plugins") -- }, -- (0, import_react8.createElement)(PlugIcon), -- t("Connect plugins"), -- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") -- ), -- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -- "span", -- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, -- t("Plugin compatibility is not fully verified.") -- ), document.body) : null ++ "button", + { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, + (0, import_react17.createElement)(PlugIcon), + t("Connect apps") + ) - ); - } - function YouAvatar() { -- return (0, import_react8.createElement)( ++ ); ++} ++function YouAvatar() { + return (0, import_react17.createElement)( - "span", - { className: "bt-you-wrap" }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ "span", ++ { className: "bt-you-wrap" }, + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) - ); - } - var openShellSettings = null; -@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { - else openHarnessSettings(); - }); - } ++ ); ++} ++var openShellSettings = null; ++var openShellOnboarding = null; ++var shellShortcuts = null; ++function holdShellShortcuts(shortcuts) { ++ shellShortcuts = shortcuts; ++ return () => { ++ if (shellShortcuts === shortcuts) shellShortcuts = null; ++ }; ++} ++function openHarnessSettings() { ++ requestAnimationFrame(() => { ++ if (openShellSettings) { ++ openShellSettings(); ++ return; ++ } ++ const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); ++ if (desktop) openThroughMenu(desktop); ++ else pressSettingsShortcut(); ++ }); ++} ++function openHarnessOnboarding(id) { ++ requestAnimationFrame(() => { ++ if (openShellOnboarding) openShellOnboarding(id); ++ else openHarnessSettings(); ++ }); ++} +var MODELS_STEP_ID = "flybot-models"; +var MODELS_STEP_ORDER = 1e3; +function ModelsSettingsStep({ explicit, complete, openSection }) { @@ -8819,179 +12185,285 @@ diff --git a/client.js b/client.js +function openHarnessModels() { + openHarnessOnboarding(MODELS_STEP_ID); +} - function openThroughMenu(trigger) { - const before = new Set(document.querySelectorAll('[role="menu"]')); - trigger.click(); -@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() { - })); - } - function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { -- const [open, setOpen] = (0, import_react8.useState)(false); -- (0, import_react8.useLayoutEffect)(() => { ++function openThroughMenu(trigger) { ++ const before = new Set(document.querySelectorAll('[role="menu"]')); ++ trigger.click(); ++ let frames = 0; ++ const look = () => { ++ const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); ++ const item = menu?.querySelector('button[role="menuitem"]'); ++ if (item) item.click(); ++ else if (++frames < 30) requestAnimationFrame(look); ++ else pressSettingsShortcut(); ++ }; ++ requestAnimationFrame(look); ++} ++function pressSettingsShortcut() { ++ const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; ++ if (!binding) return; ++ const held = new Set(binding.modifiers); ++ window.dispatchEvent(new KeyboardEvent("keydown", { ++ code: binding.code, ++ key: binding.code === "Comma" ? "," : "", ++ bubbles: true, ++ cancelable: true, ++ ctrlKey: held.has("control"), ++ altKey: held.has("alt"), ++ shiftKey: held.has("shift"), ++ metaKey: held.has("meta") ++ })); ++} ++function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { + const [open, setOpen] = (0, import_react17.useState)(false); + (0, import_react17.useLayoutEffect)(() => { - if (!seat) return void 0; - actions.seatAccount(true); - return () => actions.seatAccount(false); - }, [seat]); -- const [box, setBox] = (0, import_react8.useState)(null); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { ++ if (!seat) return void 0; ++ actions.seatAccount(true); ++ return () => actions.seatAccount(false); ++ }, [seat]); + const [box, setBox] = (0, import_react17.useState)(null); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - if (settingsOpen) setOpen(false); - }, [settingsOpen]); -- (0, import_react8.useEffect)(() => { ++ if (settingsOpen) setOpen(false); ++ }, [settingsOpen]); + (0, import_react17.useEffect)(() => { - if (!openSettings) return void 0; - openShellSettings = openSettings; - return () => { - if (openShellSettings === openSettings) openShellSettings = null; - }; - }, [openSettings]); -- (0, import_react8.useEffect)(() => { ++ if (!openSettings) return void 0; ++ openShellSettings = openSettings; ++ return () => { ++ if (openShellSettings === openSettings) openShellSettings = null; ++ }; ++ }, [openSettings]); + (0, import_react17.useEffect)(() => { - if (!openOnboarding) return void 0; - openShellOnboarding = openOnboarding; - return () => { - if (openShellOnboarding === openOnboarding) openShellOnboarding = null; - }; - }, [openOnboarding]); -- (0, import_react8.useEffect)(() => { ++ if (!openOnboarding) return void 0; ++ openShellOnboarding = openOnboarding; ++ return () => { ++ if (openShellOnboarding === openOnboarding) openShellOnboarding = null; ++ }; ++ }, [openOnboarding]); + (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { - if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi - const groups = [ - [ - [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], -- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], -- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], -- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] +- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; ++ if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; + setOpen(false); + }; + const onKey = (event) => { +@@ -5097,412 +5097,1337 @@ function UpdateButton({ useUpdate, actions }) { + window.removeEventListener("keydown", onKey, true); + }; + }, [open]); +- const phase = update?.phase ?? "idle"; +- const dsh = update?.dsh; +- const dshPhase = dsh?.phase ?? "idle"; +- const dshRow = showsDshRow(dsh, update?.app); +- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; +- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; + const toggle = () => { + const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); ++ if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); + setOpen((value) => !value); + }; +- const start = () => { +- setError(""); +- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); +- }; +- const startDsh = () => { +- setDshError(""); +- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); +- }; +- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); +- const message = error || (phase === "failed" ? update.error : ""); +- const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); +- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); +- return (0, import_react8.createElement)( +- import_react8.Fragment, ++ const groups = [ ++ [ ++ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], + [t("Connect apps"), ConnectorIcon, () => actions.openConnectors()], + [t("Plugins"), PlugIcon, () => actions.selectPanel("plugins")], + [t("Scheduled tasks"), ClockIcon, () => actions.openSettings("schedules")], + [t("Settings"), GearIcon, () => actions.openSettings("general")] - ], - [ - [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] - ] - ]; -- return (0, import_react8.createElement)( -- import_react8.Fragment, ++ ], ++ [ ++ [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] ++ ] ++ ]; + return (0, import_react17.createElement)( + import_react17.Fragment, null, -- (0, import_react8.createElement)( +- (0, import_react8.createElement)("button", { +- ref: button, +- type: "button", +- className: "bt-round bt-update", +- "data-phase": cloudPhase, +- "aria-label": label, +- title: label, +- "aria-haspopup": "dialog", +- "aria-expanded": open, +- onClick: toggle +- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + (0, import_react17.createElement)( - "button", - { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ "button", ++ { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) - ), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ), + open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)( "div", - { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, -- groups.map((group, index) => (0, import_react8.createElement)( +- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, +- update?.available || phase !== "idle" ? (0, import_react8.createElement)( - import_react8.Fragment, +- null, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), +- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-update-actions" }, +- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, +- phase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: phase === "installing", +- onClick: start +- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null, +- dshRow ? (0, import_react8.createElement)( ++ { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, + (0, import_react17.createElement)(FlyBotAccountHeader, { actions }), + groups.map((group, index) => (0, import_react17.createElement)( + import_react17.Fragment, - { key: index }, -- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { ++ { key: index }, + index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", { - key: label, - type: "button", - role: "menuitem", -@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi - setOpen(false); - void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); - } -- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) -+ }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) - )) - ), document.body) : null - ); -@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) { - const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); - return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; - }); -- const lastBot = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { ++ key: label, ++ type: "button", ++ role: "menuitem", ++ onClick: () => { ++ setOpen(false); ++ void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); ++ } ++ }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) ++ )) ++ ), document.body) : null ++ ); ++} ++function useLandOnMainBot(roster, useSessions, actions) { ++ const mainView = useSessions((list) => { ++ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); ++ return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; ++ }); + const lastBot = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { - if (!roster.ready || !roster.mainBotId) return void 0; - const [id, blank3] = mainView.split("|"); - const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) { - return void 0; - } - const last = lastBot.current; -- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); ++ if (!roster.ready || !roster.mainBotId) return void 0; ++ const [id, blank3] = mainView.split("|"); ++ const bot = id === "" ? void 0 : roster.byId[id]; ++ if (bot !== void 0) { ++ lastBot.current = { botId: bot.id, sessionId: id }; ++ if (currentPart(bot) !== id) actions.landOn(bot.id); ++ return void 0; ++ } ++ if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { ++ lastBot.current = null; ++ return void 0; ++ } ++ const last = lastBot.current; + const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400); - return () => clearTimeout(timer); - }, [mainView, roster]); - } - function FooterAccount(props) { - const seated = props.useUi((value) => value.accountSeated === true); - if (seated) return null; -- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); ++ return () => clearTimeout(timer); ++ }, [mainView, roster]); ++} ++function FooterAccount(props) { ++ const seated = props.useUi((value) => value.accountSeated === true); ++ if (seated) return null; + return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false })); - } - function BotReturn({ wide, useSurface, actions }) { - const mode = useSurface((value) => value.mode); - if (mode !== "agent") return null; -- return (0, import_react8.createElement)( ++} ++function BotReturn({ wide, useSurface, actions }) { ++ const mode = useSurface((value) => value.mode); ++ if (mode !== "agent") return null; + return (0, import_react17.createElement)( - "button", -- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, -- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), -- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null ++ "button", + { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, + (0, import_react17.createElement)(FlyBotMark, { size: 20 }), + wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null - ); - } - var RETURN_CSS = ` - .bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} - .bt-return:hover{background:rgba(127,127,127,.14)} - .bt-return:active{transform:scale(.96)} --.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} --.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} ++ ); ++} ++var RETURN_CSS = ` ++.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} ++.bt-return:hover{background:rgba(127,127,127,.14)} ++.bt-return:active{transform:scale(.96)} +.bt-return .bt-mark,.bt-return .bt-flybot-mark{display:inline-flex;flex:none;line-height:0} +.bt-return .bt-mark svg,.bt-return .bt-flybot-mark{width:20px;height:20px;overflow:visible} - /* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ - .bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} - .bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - `; - function BrandMark({ size = 24 }) { -- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); ++/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ ++.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} ++.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++`; ++function BrandMark({ size = 24 }) { + return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size })); - } - function BrandName() { -- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); ++} ++function BrandName() { + return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); - } - var SIDEBAR_CSS = ` - .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5456,7 +5456,7 @@ var SIDEBAR_CSS = ` - .bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} - @keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} - .bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++} ++var SIDEBAR_CSS = ` ++.bt-side-top .bt-brand{cursor:default;padding-left:6px} ++.bt-side-top .bt-brand:hover{background:none} ++.bt-brand .bt-mark{flex:none} ++.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} ++.bt-rail .bt-row{position:relative} ++.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} ++.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} ++[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} ++[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} ++.bt-foot-account{order:-1;flex:none;display:inline-flex} ++[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} ++[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} ++[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} ++ ++/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a ++ loop while the install runs. */ ++.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-round.bt-update svg{width:17px;height:17px} ++.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} ++@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} ++.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} ++@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} ++.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +.bt-update-pop{position:fixed;z-index:130;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} - @keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} - .bt-update-title{font-size:14px;line-height:20px;font-weight:600} - .bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} -@@ -5491,18 +5491,1177 @@ var SIDEBAR_CSS = ` - .bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} - @keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} - @media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} ++@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} ++.bt-update-title{font-size:14px;line-height:20px;font-weight:600} ++.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} ++.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} ++.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} ++.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} ++.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} ++.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} ++.bt-update-go:hover:not(:disabled){opacity:.9} ++.bt-update-go:active:not(:disabled){transform:scale(.96)} ++.bt-update-go:disabled{opacity:.6;cursor:default} ++.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} ++@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} ++ ++/* The DSH Agent row and its expanding Session list. The black whale is the ++ harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ ++.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} ++.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} ++@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} ++.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-agent-sub[data-open]{grid-template-rows:1fr} ++.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} ++.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} ++.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} ++.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} ++.bt-agent-new{color:var(--bt-ink-3)} ++.bt-agent-new svg{width:14px;height:14px;flex:none} ++.bt-agent-edit{padding:3px 10px 3px 4px} ++.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} ++.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} ++@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} ++@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} + +.bt-row-wrap{position:relative;display:flex;align-items:center;width:100%;min-width:0} +.bt-row-wrap>.bt-row{flex:1 1 auto;min-width:0;width:100%} @@ -9088,10 +12560,23 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-astack" }, + (0, import_react18.createElement)( -+ "div", + "div", +- { className: "bt-update-dsh" }, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), +- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, +- (0, import_react8.createElement)( + { className: "bt-msg" }, + (0, import_react18.createElement)( -+ "div", + "div", +- { className: "bt-update-actions" }, +- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { +- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: dshPhase === "installing", +- onClick: startDsh +- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) + { className: "bt-msg-line" }, + text2 ? (0, import_react18.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react18.createElement)( + "div", @@ -9100,7 +12585,10 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("span", null), + (0, import_react18.createElement)("span", null) + ) -+ ) + ) +- ) : null +- ), document.body) : null +- ); + ) + ) + ), host); @@ -9114,8 +12602,8 @@ diff --git a/client.js b/client.js +.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s} +@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}} +@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}} - `; - ++`; ++ +// src/client/project-board.js +var import_react19 = require("react"); +var PROJECT_BOARD_CSS = ` @@ -9310,7 +12798,24 @@ diff --git a/client.js b/client.js + emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." + } + }; -+} + } +-function ConnectPlugins({ wide, actions }) { +- const tipId = (0, import_react8.useId)(); +- const [tip, setTip] = (0, import_react8.useState)(false); +- const timer = (0, import_react8.useRef)(0); +- const button = (0, import_react8.useRef)(null); +- const [box, setBox] = (0, import_react8.useState)(null); +- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); +- if (!wide) return null; +- const show = () => { +- clearTimeout(timer.current); +- timer.current = setTimeout(() => { +- const rect = button.current?.getBoundingClientRect(); +- if (!rect) return; +- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); +- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); +- setTip(true); +- }, 250); +function projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics } = {}) { + const isRow = (row) => row !== null && typeof row === "object" && text(row.id) !== ""; + const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); @@ -9343,7 +12848,10 @@ diff --git a/client.js b/client.js + speaksFor: text(row.speaksFor) || null, + board: boardOfRow(row, task) + }; -+ }; + }; +- const hide = () => { +- clearTimeout(timer.current); +- setTip(false); + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], @@ -9380,7 +12888,28 @@ diff --git a/client.js b/client.js + emptyText: "No cards yet.", + record: projectBoardRecord({ decisions, meetings }), + metrics: metricsView(metrics, { tasks }) -+ }; + }; +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-connect-wrap" }, +- (0, import_react8.createElement)( +- "button", +- { +- ref: button, +- type: "button", +- className: "bt-connect bt-connect-foot", +- "aria-describedby": tip ? tipId : void 0, +- onMouseEnter: show, +- onMouseLeave: hide, +- onFocus: show, +- onBlur: hide, +- onClick: () => actions.selectPanel("plugins") +- }, +- (0, import_react8.createElement)(PlugIcon), +- t("Connect plugins"), +- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") +- ), +- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( +} +function projectRowNode(row, onOpen) { + const open = row.board === null ? void 0 : () => onOpen?.(row.board); @@ -9401,7 +12930,10 @@ diff --git a/client.js b/client.js + }, + (0, import_react19.createElement)("span", { className: "bt-project-column" }, t(row.column)), + (0, import_react19.createElement)( -+ "span", + "span", +- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, +- t("Plugin compatibility is not fully verified.") +- ), document.body) : null + { className: "bt-project-title-wrap" }, + (0, import_react19.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")), + // The audit text the row already carries (#1730): shown inside the card detail, never a @@ -9414,8 +12946,14 @@ diff --git a/client.js b/client.js + row.speaksFor === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-speaks-for", "data-speaks-for": row.speaksFor }, t("for {user}", { user: row.speaksFor })), + row.priority === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-priority" }, row.priority), + row.runId === null ? null : (0, import_react19.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) -+ ); -+} + ); + } +-function YouAvatar() { +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-you-wrap" }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), +- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) +function ProjectBoardRows({ view, onOpen }) { + if (view.empty) return (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); + return view.groups.map((group) => (0, import_react19.createElement)( @@ -9432,8 +12970,15 @@ diff --git a/client.js b/client.js + { className: "bt-project-group bt-project-done", "data-group": "done" }, + (0, import_react19.createElement)("summary", { className: "bt-project-done-summary" }, `${t(view.done.label)} · ${view.done.rows.length}`), + view.done.rows.map((row) => projectRowNode(row, onOpen)) -+ ); -+} + ); + } +-var openShellSettings = null; +-var openShellOnboarding = null; +-var shellShortcuts = null; +-function holdShellShortcuts(shortcuts) { +- shellShortcuts = shortcuts; +- return () => { +- if (shellShortcuts === shortcuts) shellShortcuts = null; +function waitedLabel(ms) { + if (!Number.isFinite(ms) || ms < 0) return null; + if (ms < 6e4) return `${ms === 0 ? 0 : Math.max(1, Math.floor(ms / 1e3))}s`; @@ -9487,8 +13032,18 @@ diff --git a/client.js b/client.js + running, + empty: source === null, + emptyText: "No metrics read yet." -+ }; -+} + }; + } +-function openHarnessSettings() { +- requestAnimationFrame(() => { +- if (openShellSettings) { +- openShellSettings(); +- return; +- } +- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); +- if (desktop) openThroughMenu(desktop); +- else pressSettingsShortcut(); +- }); +function ProjectMetrics({ view }) { + return (0, import_react19.createElement)( + "section", @@ -9506,7 +13061,12 @@ diff --git a/client.js b/client.js + )) + ) + ); -+} + } +-function openHarnessOnboarding(id) { +- requestAnimationFrame(() => { +- if (openShellOnboarding) openShellOnboarding(id); +- else openHarnessSettings(); +- }); +function ProjectRunningTable({ view, onOpen }) { + if (view.running.length === 0) return (0, import_react19.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react19.createElement)("h4", null, t("Running tasks")), (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); + return (0, import_react19.createElement)( @@ -9545,7 +13105,17 @@ diff --git a/client.js b/client.js + ); + }) + ); -+} + } +-function openThroughMenu(trigger) { +- const before = new Set(document.querySelectorAll('[role="menu"]')); +- trigger.click(); +- let frames = 0; +- const look = () => { +- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); +- const item = menu?.querySelector('button[role="menuitem"]'); +- if (item) item.click(); +- else if (++frames < 30) requestAnimationFrame(look); +- else pressSettingsShortcut(); +function ProjectQueueGroups({ view, onOpen }) { + return view.queues.map((entry) => (0, import_react19.createElement)( + "section", @@ -9647,8 +13217,23 @@ diff --git a/client.js b/client.js + // `project-read` record answers no rank, and the panel never re-derives the host's policy. + role: text(project?.role), + choices: idList(project?.choices) -+ }; -+} + }; +- requestAnimationFrame(look); + } +-function pressSettingsShortcut() { +- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; +- if (!binding) return; +- const held = new Set(binding.modifiers); +- window.dispatchEvent(new KeyboardEvent("keydown", { +- code: binding.code, +- key: binding.code === "Comma" ? "," : "", +- bubbles: true, +- cancelable: true, +- ctrlKey: held.has("control"), +- altKey: held.has("alt"), +- shiftKey: held.has("shift"), +- metaKey: held.has("meta") +- })); +var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react19.createElement)( + "div", + { @@ -9732,7 +13317,44 @@ diff --git a/client.js b/client.js + ) + ) + ); -+} + } +-function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { +- const [open, setOpen] = (0, import_react8.useState)(false); +- (0, import_react8.useLayoutEffect)(() => { +- if (!seat) return void 0; +- actions.seatAccount(true); +- return () => actions.seatAccount(false); +- }, [seat]); +- const [box, setBox] = (0, import_react8.useState)(null); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (settingsOpen) setOpen(false); +- }, [settingsOpen]); +- (0, import_react8.useEffect)(() => { +- if (!openSettings) return void 0; +- openShellSettings = openSettings; +- return () => { +- if (openShellSettings === openSettings) openShellSettings = null; +- }; +- }, [openSettings]); +- (0, import_react8.useEffect)(() => { +- if (!openOnboarding) return void 0; +- openShellOnboarding = openOnboarding; +- return () => { +- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; +- }; +- }, [openOnboarding]); +- (0, import_react8.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; +- setOpen(false); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); +function dossierRowOf(team, botId) { + const id = text(botId); + if (id === "") return null; @@ -9803,11 +13425,41 @@ diff --git a/client.js b/client.js + target = botIdOf(record?.project?.assistant); + } catch { + target = ""; -+ } + } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +- const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); +- setOpen((value) => !value); + if (target !== "") setAgentId(target); + } + if (target !== "") onOpen?.(target); -+ }; + }; +- const groups = [ +- [ +- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], +- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], +- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], +- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] +- ], +- [ +- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] +- ] +- ]; +- return (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)( +- "button", +- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + if (id === "") return null; + return (0, import_react19.createElement)("button", { + type: "button", @@ -9895,9 +13547,17 @@ diff --git a/client.js b/client.js + (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")), + (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind")) + ) -+ ), + ), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + (0, import_react19.createElement)( -+ "div", + "div", +- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, +- groups.map((group, index) => (0, import_react8.createElement)( +- import_react8.Fragment, +- { key: index }, +- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { +- key: label, + { className: "bt-project-field" }, + (0, import_react19.createElement)("label", null, t("Project Assistant")), + (0, import_react19.createElement)( @@ -9919,16 +13579,20 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-project-row-actions" }, + (0, import_react19.createElement)("button", { -+ type: "button", + type: "button", +- role: "menuitem", + className: "bt-soft bt-soft-sm", + "data-action": "project-memory-save", + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. -+ onClick: () => { + onClick: () => { +- setOpen(false); +- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); + if (noteSaveDisabled) return; + run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")); -+ } + } +- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + }, t("Save memory")) + ) + ), @@ -9978,7 +13642,8 @@ diff --git a/client.js b/client.js + { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, + (0, import_react19.createElement)("span", null, `@${name}`), + (0, import_react19.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) -+ )) + )) +- ), document.body) : null + ), + owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react19.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, + (0, import_react19.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")), @@ -9987,8 +13652,25 @@ diff --git a/client.js b/client.js + { className: "bt-project-row-actions" }, + (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) -+ ); -+} + ); + } +-function useLandOnMainBot(roster, useSessions, actions) { +- const mainView = useSessions((list) => { +- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); +- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; +- }); +- const lastBot = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!roster.ready || !roster.mainBotId) return void 0; +- const [id, blank3] = mainView.split("|"); +- const bot = id === "" ? void 0 : roster.byId[id]; +- if (bot !== void 0) { +- lastBot.current = { botId: bot.id, sessionId: id }; +- if (currentPart(bot) !== id) actions.landOn(bot.id); +- return void 0; +- } +- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { +- lastBot.current = null; +async function readProjectBoard(read, wanted, cached) { + const listing = await read("projects"); + const projects = Array.isArray(listing?.projects) ? listing.projects : []; @@ -10055,8 +13737,26 @@ diff --git a/client.js b/client.js + if (typeof window === "undefined") return void 0; + if (!hash.startsWith("#project")) { + setState(null); -+ return void 0; -+ } + return void 0; + } +- const last = lastBot.current; +- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); +- return () => clearTimeout(timer); +- }, [mainView, roster]); +-} +-function FooterAccount(props) { +- const seated = props.useUi((value) => value.accountSeated === true); +- if (seated) return null; +- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); +-} +-function BotReturn({ wide, useSurface, actions }) { +- const mode = useSurface((value) => value.mode); +- if (mode !== "agent") return null; +- return (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, +- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), +- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null + const wanted = hash.slice("#project".length).replace(/^=/, ""); + let gone = false; + let teamAnswer; @@ -10150,9 +13850,85 @@ diff --git a/client.js b/client.js + }) + ) + ) -+ ); -+} -+ + ); + } +-var RETURN_CSS = ` +-.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} +-.bt-return:hover{background:rgba(127,127,127,.14)} +-.bt-return:active{transform:scale(.96)} +-.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} +-.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} +-/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ +-.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} +-.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-`; +-function BrandMark({ size = 24 }) { +- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); +-} +-function BrandName() { +- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); +-} +-var SIDEBAR_CSS = ` +-.bt-side-top .bt-brand{cursor:default;padding-left:6px} +-.bt-side-top .bt-brand:hover{background:none} +-.bt-brand .bt-mark{flex:none} +-.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} +-.bt-rail .bt-row{position:relative} +-.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} +-.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +-[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} +-.bt-foot-account{order:-1;flex:none;display:inline-flex} +-[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} +- +-/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a +- loop while the install runs. */ +-.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-round.bt-update svg{width:17px;height:17px} +-.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} +-@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} +-.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} +-@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} +-.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-.bt-update-title{font-size:14px;line-height:20px;font-weight:600} +-.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} +-.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} +-.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} +-.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} +-.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} +-.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} +-.bt-update-go:hover:not(:disabled){opacity:.9} +-.bt-update-go:active:not(:disabled){transform:scale(.96)} +-.bt-update-go:disabled{opacity:.6;cursor:default} +-.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} +-@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} +- +-/* The DSH Agent row and its expanding Session list. The black whale is the +- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ +-.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} +-@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} +-.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-agent-sub[data-open]{grid-template-rows:1fr} +-.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} +-.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} +-.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} +-.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} +-.bt-agent-new{color:var(--bt-ink-3)} +-.bt-agent-new svg{width:14px;height:14px;flex:none} +-.bt-agent-edit{padding:3px 10px 3px 4px} +-.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} +-.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} +-@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} +-@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} +-`; + // src/client/header.js -var import_react9 = require("react"); -var import_react_dom4 = require("react-dom"); @@ -10259,15 +14035,14 @@ diff --git a/client.js b/client.js - (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), - (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), - (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) -- ), -- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), -- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) + bot ? (0, import_react20.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react20.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react20.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), + (0, import_react20.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react20.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react20.createElement)(GoArrow)) -+ ), + ), +- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), +- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), +- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) + (0, import_react20.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + projectAgent === null ? null : (0, import_react20.createElement)(ProjectAgentEntry, { + projectId: projectAgent.projectId, @@ -13235,7 +17010,7 @@ diff --git a/client.js b/client.js const isMainBot = isMainOf(roster, bot.id); const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; const isTile = roster.mainBotId === bot.id; -@@ -8011,112 +8011,116 @@ function BotSettingsForm({ bot, roster, actions, run }) { +@@ -8011,149 +8011,812 @@ function BotSettingsForm({ bot, roster, actions, run }) { if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -13340,73 +17115,59 @@ diff --git a/client.js b/client.js + (0, import_react30.createElement)(BotSchedules, { bot, roster, actions }), + (0, import_react30.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), + (0, import_react30.createElement)( - Card, - { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( ++ Card, ++ { title: t("Main Bot") }, + onlyMain ? (0, import_react30.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( -+ (0, import_react30.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ "div", ++ { className: "bt-set-row bt-bs-row" }, ++ (0, import_react30.createElement)( ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) - ), -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) ++ ), + (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) + ) : (0, import_react30.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) - ), - // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react18.createElement)( ++ ), ++ // The Main Bot tile is always first and always shown, like in the context menu. + isTile ? null : (0, import_react30.createElement)( - Card, - { title: t("Sidebar") }, -- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) ++ Card, ++ { title: t("Sidebar") }, + (0, import_react30.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), + isMainBot ? null : (0, import_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react30.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( ++ Card, + { title: t("More") }, + (0, import_react30.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, + (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), + (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) - ), -- isMainBot ? null : (0, import_react18.createElement)( ++ ), + isMainBot ? null : (0, import_react30.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + (0, import_react30.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), + (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) - ), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ ), + (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) - ) - ) - ); -@@ -8130,9 +8130,17 @@ var BOT_SETTINGS_CSS = ` - .bt-bs .bt-avedit{left:0;transform:none} - .bt-bs-row{min-height:48px} - label.bt-bs-row{cursor:text} --.bt-bs-row>.bt-set-copy{flex:1 1 auto} - .bt-bs-label{font-size:13px;line-height:18px;font-weight:500} ++ ) ++ ) ++ ); ++} ++var BOT_SETTINGS_CSS = ` ++.bt-bs{display:flex;flex-direction:column;gap:18px} ++.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} ++.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} ++.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-bs .bt-avedit{left:0;transform:none} ++.bt-bs-row{min-height:48px} ++label.bt-bs-row{cursor:text} ++.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to + break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test + connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under @@ -13414,16 +17175,29 @@ diff --git a/client.js b/client.js +.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} +.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} - .bt-bs-row .bt-model-trigger{flex:none;max-width:58%} ++.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +${MODEL_TEST_CSS} - .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} - .bt-bs-input:hover{background:var(--bt-hover)} - .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -@@ -8153,8 +8153,659 @@ label.bt-bs-row{cursor:text} - .bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} - .bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} - @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} ++.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} ++.bt-bs-input:hover{background:var(--bt-hover)} ++.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} ++.bt-bs-input::placeholder{color:var(--bt-ink-3)} ++.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} ++.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} ++.bt-bs-area:hover{background:var(--bt-active)} ++.bt-bs-danger .bt-bs-label{color:#c21d2e} ++.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} ++.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} ++.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} ++.bt-switch[aria-checked=true]{background:var(--bt-accent)} ++.bt-switch:disabled{opacity:.45;cursor:default} ++.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} ++.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} ++.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} ++.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} ++.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} ++.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} ++@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below + 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and + keeps its own words. Nothing is clipped to fit a width that is not there. */ @@ -13666,13 +17440,20 @@ diff --git a/client.js b/client.js + people2.map((person) => (0, import_react31.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) + ) : null, + face ? null : (0, import_react31.createElement)( -+ Card, + Card, +- { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, + humans.map((person) => (0, import_react31.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( + { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, + (0, import_react31.createElement)( -+ "span", + "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) + { className: "bt-gs-who" }, + (0, import_react31.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), + (0, import_react31.createElement)("span", { className: "bt-tag" }, t("Human")), @@ -13712,7 +17493,9 @@ diff --git a/client.js b/client.js + // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is + // labelled as such; a Bot with no Bot Social identity can never be written to at all. + room2.origin === "face-to-face" ? null : (0, import_react31.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) -+ ), + ), +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) +- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) + // A guest's mirror is not theirs to edit: the host's membership is changed on the host side. + room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") + )), @@ -13757,26 +17540,43 @@ diff --git a/client.js b/client.js + // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after + // the note on who leads, so that note stays with the members it is about (#892). + room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react31.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) -+ ), + ), +- // The Main Bot tile is always first and always shown, like in the context menu. +- isTile ? null : (0, import_react18.createElement)( + (0, import_react31.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), + (0, import_react31.createElement)( -+ Card, -+ { title: t("Sidebar") }, + Card, + { title: t("Sidebar") }, +- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), +- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) + (0, import_react31.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), + (0, import_react31.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) -+ ), + ), +- (0, import_react18.createElement)( + (0, import_react31.createElement)( -+ Card, + Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) +- ), +- isMainBot ? null : (0, import_react18.createElement)( + { title: t("More") }, + (0, import_react31.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + "button", + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react18.createElement)( + (0, import_react31.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "span", + { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) + (0, import_react31.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), + (0, import_react31.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -+ ), + ), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + (0, import_react31.createElement)("span", { className: "bt-chevron" }, (0, import_react31.createElement)(ChevronRightIcon)) + ) + ) @@ -13876,8 +17676,8 @@ diff --git a/client.js b/client.js +.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out} +.bt-bs-stack .bt-seg{align-self:stretch} +@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} - `; - ++`; ++ +// src/client/palette.js +var import_react32 = require("react"); +var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -14072,14 +17872,46 @@ diff --git a/client.js b/client.js + ); + }) + )) -+ ) -+ ) -+ ); -+} -+ + ) + ) + ); + } +-var BOT_SETTINGS_CSS = ` +-.bt-bs{display:flex;flex-direction:column;gap:18px} +-.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} +-.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} +-.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-bs .bt-avedit{left:0;transform:none} +-.bt-bs-row{min-height:48px} +-label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} +-.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +-.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +-.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} +-.bt-bs-input:hover{background:var(--bt-hover)} +-.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +-.bt-bs-input::placeholder{color:var(--bt-ink-3)} +-.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} +-.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} +-.bt-bs-area:hover{background:var(--bt-active)} +-.bt-bs-danger .bt-bs-label{color:#c21d2e} +-.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} +-.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} +-.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} +-.bt-switch[aria-checked=true]{background:var(--bt-accent)} +-.bt-switch:disabled{opacity:.45;cursor:default} +-.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} +-.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} +-.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} +-.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} +-.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} +-.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} +-@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +-`; + // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { - const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); @@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { } function startModel(roster) { @@ -15155,6 +18987,49 @@ diff --git a/client.js b/client.js - { className: "bt-bs-hero" }, - (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - (0, import_react23.createElement)( +- "div", +- { className: "bt-bs-who" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), +- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Profile") }, +- (0, import_react23.createElement)( +- Row, +- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, +- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) +- ), +- (0, import_react23.createElement)( +- Row, +- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, +- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Who answers") }, +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { +- key: id, +- type: "button", +- "aria-pressed": mode === id, +- disabled: id === "admin" && !admin, +- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, +- onClick: () => { +- if (mode !== id) void update({ mode: id }); +- } +- }, t(label)))), +- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Working folder") }, +- (0, import_react23.createElement)( + { className: "bt-wf-palette" }, + (0, import_react37.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), + locals.map((bot) => (0, import_react37.createElement)("button", { @@ -15191,12 +19066,71 @@ diff --git a/client.js b/client.js + { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, + (0, import_react37.createElement)( "div", -- { className: "bt-bs-who" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), -- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Members · {count}", { count: members.length }) }, +- members.map((member) => { +- const leads = room2.admin === member.id; +- return (0, import_react23.createElement)( + { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, + (0, import_react37.createElement)( -+ "div", + "div", +- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, +- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), +- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), +- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null +- ), +- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), +- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null +- ); +- }), +- adding ? [ +- ...outside.map((bot) => (0, import_react23.createElement)( +- "button", +- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { +- setAdding(false); +- void update({ add: [bot.id] }); +- } }, +- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) +- )), +- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) +- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, +- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( +- "span", +- { className: "bt-set-hint" }, +- // The host checks for two members on every change, even a rename. +- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") +- )) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), +- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) +- ), +- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) + { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } }, + (0, import_react37.createElement)( + "svg", @@ -15288,9 +19222,38 @@ diff --git a/client.js b/client.js + }) + ) ) -+ ) -+ ); -+} + ) + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function Row2({ item, source, onOpen, onRename, onDelete }) { + return (0, import_react37.createElement)( + "button", @@ -15306,14 +19269,7 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react37.createElement)("span", null, item.name), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") - ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Profile") }, -- (0, import_react23.createElement)( -- Row, -- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, -- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) ++ ), + source === "own" ? (0, import_react37.createElement)( + "span", + { className: "bt-set-actions" }, @@ -15352,11 +19308,19 @@ diff --git a/client.js b/client.js + const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); + const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; + return (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( + { className: "bt-wf-page" }, + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")), + error ? (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + { className: "bt-error", role: "alert" }, + alertCopy(error), + phase === "error" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null @@ -15380,11 +19344,7 @@ diff --git a/client.js b/client.js + return (0, import_react37.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") }); + }) + ) - ), -- (0, import_react23.createElement)( -- Row, -- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) ++ ), + (0, import_react37.createElement)( + "section", + { className: "bt-set-section" }, @@ -15397,34 +19357,18 @@ diff --git a/client.js b/client.js + ) ) - ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Who answers") }, -- (0, import_react23.createElement)( +- admin === null ? null : (0, import_react23.createElement)( + ) : null, + graph ? (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), +- (0, import_react23.createElement)( + { className: "bt-wf-editor" }, + (0, import_react37.createElement)( "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": mode === id, -- disabled: id === "admin" && !admin, -- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, -- onClick: () => { -- if (mode !== id) void update({ mode: id }); -- } -- }, t(label)))), -- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react23.createElement)( +- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, +- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) + { className: "bt-wf-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), + showUse ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, @@ -15460,153 +19404,42 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length }) }, -- members.map((member) => { -- const leads = room2.admin === member.id; -- return (0, import_react23.createElement)( -- "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null -- ); -- }), -- adding ? [ -- ...outside.map((bot) => (0, import_react23.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ add: [bot.id] }); -- } }, -- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host checks for two members on every change, even a rename. -- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") -- )) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), -- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Manage") }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -- ), -- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) ++ "div", + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) - ) - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react23.useState)(null); -- const [members, setMembers] = (0, import_react23.useState)([]); -- const [name, setName] = (0, import_react23.useState)(""); -- const [busy, setBusy] = (0, import_react23.useState)(false); -- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const pickAdmin = (id) => { -- setAdmin(id); -- setMembers((list) => list.filter((item) => item !== id)); -- }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -- setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -- }; -- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); -- return (0, import_react23.createElement)( +- ), +- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) +- ), +- (0, import_react23.createElement)( ++ ) ++ ); ++} +function DeleteModal({ workflow, onClose, onConfirm }) { + return (0, import_react37.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) -- ) -- ), -- admin === null ? null : (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, + (0, import_react37.createElement)("h3", null, t("Delete workflow")), + (0, import_react37.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), + (0, import_react37.createElement)( - "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) ++ "div", + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) - ) -- ), -- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ ) ) ); } @@ -16028,7 +19861,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick +@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -17584,7 +21417,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false +@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -18002,10 +21835,22 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11038,244 +11038,8 @@ function migrateStorage(storage = globalThis.localStorage) { +@@ -11026,256 +11026,20 @@ function migrateStorage(storage = globalThis.localStorage) { + } } - } - + try { +- migrateBrowsers(storage); +- } catch { +- } +- try { +- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); +- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { +- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); +- } +- } catch { +- } +-} +- -// src/client/member-status.js -var import_react30 = require("react"); -var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -18171,7 +22016,16 @@ diff --git a/client.js b/client.js - (0, import_react30.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), - entry.approvals.map((approval) => (0, import_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) - ); -- } ++ migrateBrowsers(storage); ++ } catch { ++ } ++ try { ++ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); ++ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { ++ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); ++ } ++ } catch { + } - if (entry.state !== "work") return null; - const where = entry.doing.roomId === room2.id ? "" : entry.doing.roomId ? t("in {name}", { name: roster.roomsById[entry.doing.roomId]?.name ?? t("a group chat") }) : t("in its own chat"); - const stop = () => { @@ -18194,7 +22048,7 @@ diff --git a/client.js b/client.js - const entries2 = room2 ? memberWork(room2, work) : []; - const now = useNow2(entries2.some((entry) => entry.state !== "idle")); - return { entries: entries2, now }; --} + } -var MEMBER_CSS = ` -.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} -.bt-mw-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} @@ -18230,7 +22084,7 @@ diff --git a/client.js b/client.js -@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} -@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} -`; -- + // src/client/details-drawer.js -var import_react31 = require("react"); -function transcriptOf(list, id, name, hiddenTurns = []) { @@ -18370,11 +22224,8 @@ diff --git a/client.js b/client.js useMorph(body, open); - return (0, import_react31.createElement)( + return (0, import_react44.createElement)( - "div", - null, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ "div", ++ null, + (0, import_react44.createElement)("div", { className: "bt-section-title" }, t(title)), + (0, import_react44.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), + long ? (0, import_react44.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null @@ -18427,8 +22278,11 @@ diff --git a/client.js b/client.js + const people2 = facePeople(room2, roster); + const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); + return (0, import_react44.createElement)( -+ "div", -+ null, + "div", + null, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), +- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), +- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null + (0, import_react44.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), + (0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), + (0, import_react44.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react44.createElement)(FacePersonRow, { @@ -18934,7 +22788,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -18991,7 +22845,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) } }) ), @@ -19139,7 +22993,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roste +@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu if (event.target === event.currentTarget) setLanded(true); } }, @@ -19902,7 +23756,7 @@ diff --git a/client.js b/client.js + const { turns, steps } = params; + return text2 === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text2; + }; -+} + } +function statsDock(entriesOf, id = STATS_ID) { + const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id); + function StatsDock(props) { @@ -19912,7 +23766,7 @@ diff --git a/client.js b/client.js + return (0, import_react49.createElement)(entry.component, { ...props, t: t2 }); + } + return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} }; - } ++} +var statsDocks = (entriesOf) => STATS_IDS.map((id) => ({ id, ...statsDock(entriesOf, id) })); // src/client/index.js @@ -20378,7 +24232,7 @@ diff --git a/client.js b/client.js shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice), shadow("conversation.composer", { priority: -10, -@@ -14074,9 +14074,18 @@ function apply(ctx) { +@@ -14074,8 +14074,17 @@ function apply(ctx) { shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock), shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock), shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock), @@ -20386,7 +24240,7 @@ diff --git a/client.js b/client.js + shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock), + shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock) ]; - ctx.effect(() => { ++ ctx.effect(() => { + const style = installStyles(TURN_FAILURES_CSS, "turn-failures"); + const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react50.createElement)(TurnFailures, { ...props, work }) }); + return () => { @@ -20394,7 +24248,6 @@ diff --git a/client.js b/client.js + style.remove(); + }; + }, "ds-bot: dead turn lines"); -+ ctx.effect(() => { + ctx.effect(() => { const style = installStyles(RETURN_CSS, "return"); return () => style.remove(); - }, "ds-bot: return styles"); From ce4b2774607e5c9e921519b0047cb6d61ba1cdfe Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 31 Oct 2026 15:09:12 +0800 Subject: [PATCH 5/6] #1900: drop the throwaway regenerate workflow patches/ds-bot/0008-client-bundle.patch is now what `node scripts/ds-bot-patches.mjs generate` writes (that is what the removed workflow ran, with git, and its canonical step was green). The workflow was a one-shot to get git into the loop; it is not part of the change. Docs: docs/subsystems/core-client.md is updated in this branch. --- .github/workflows/regen-1900-patches.yml | 27 ------------------------ 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/regen-1900-patches.yml diff --git a/.github/workflows/regen-1900-patches.yml b/.github/workflows/regen-1900-patches.yml deleted file mode 100644 index d9a1bade0..000000000 --- a/.github/workflows/regen-1900-patches.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Regen 1900 patches -on: - push: - branches: [dev/1900-dossier-off-poll] -permissions: - contents: write -jobs: - regen: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: dev/1900-dossier-off-poll - fetch-depth: 0 - - name: generate - run: node scripts/ds-bot-patches.mjs generate - - name: commit the series git would write - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - if git diff --quiet -- patches/ds-bot; then echo "nothing to regenerate"; exit 0; fi - git add patches/ds-bot - git commit -m "Regenerate patches/ds-bot from core/ (generate, #1900)" - git push origin HEAD:dev/1900-dossier-off-poll - - name: canonical - run: node scripts/ds-bot-patches.mjs canonical From 02b0771083cbc327e6b2ee4fe6bdd09ff064deea Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 31 Oct 2026 15:14:03 +0800 Subject: [PATCH 6/6] #1900: a poll tick is four reads, not five The #1814 test pinned the poll at five reads a tick, the dossier included. With the dossier read once at open, a tick is the four channels that change; the test now says so at the three places it counts. Docs: docs/subsystems/core-client.md carries the change from the parent commits. --- core/test/project-board-view.test.js | 8 +++++--- patches/ds-bot/files/test/project-board-view.test.js | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/test/project-board-view.test.js b/core/test/project-board-view.test.js index c4310a890..9728903c8 100644 --- a/core/test/project-board-view.test.js +++ b/core/test/project-board-view.test.js @@ -353,7 +353,9 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b assert.match(textIn(tree), /Dev4 · stalled/, 'the board draws what the first read answered') const afterFirst = count.calls await tick() - assert.equal(count.calls, afterFirst + 5, 'an open board reads again on the next tick') + // #1900: a tick is the four reads that change — the dossier was read once above and is drawn from + // that answer, so it is not part of a tick's count. + assert.equal(count.calls, afterFirst + 4, 'an open board reads the four that change on the next tick, not the dossier') // A row opens its own board: the hash names the task and this board stops drawing, so a tick that // lands before the cleanup must not read. This segment pins the unmount, not the predicate. const afterTick = count.calls @@ -363,7 +365,7 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b // Closing that board puts this one back in front: the next tick reads again. win.location.hash = '#project' await tick() - assert.equal(count.calls, afterTick + 5, 'closing the row’s board resumes the poll') + assert.equal(count.calls, afterTick + 4, 'closing the row’s board resumes the poll') // A hidden tab reads nothing either — this is the segment the named predicate is pinned by. const afterResume = count.calls doc.visibilityState = 'hidden' @@ -371,7 +373,7 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b assert.equal(count.calls, afterResume, 'no read in a hidden tab') doc.visibilityState = 'visible' await tick() - assert.equal(count.calls, afterResume + 5, 'a visible tab reads again on the next tick') + assert.equal(count.calls, afterResume + 4, 'a visible tab reads again on the next tick') // Close the board: the timer goes with it, so a tick afterwards reads nothing at all. const afterVisible = count.calls for (const off of cleanups) off() diff --git a/patches/ds-bot/files/test/project-board-view.test.js b/patches/ds-bot/files/test/project-board-view.test.js index c4310a890..9728903c8 100644 --- a/patches/ds-bot/files/test/project-board-view.test.js +++ b/patches/ds-bot/files/test/project-board-view.test.js @@ -353,7 +353,9 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b assert.match(textIn(tree), /Dev4 · stalled/, 'the board draws what the first read answered') const afterFirst = count.calls await tick() - assert.equal(count.calls, afterFirst + 5, 'an open board reads again on the next tick') + // #1900: a tick is the four reads that change — the dossier was read once above and is drawn from + // that answer, so it is not part of a tick's count. + assert.equal(count.calls, afterFirst + 4, 'an open board reads the four that change on the next tick, not the dossier') // A row opens its own board: the hash names the task and this board stops drawing, so a tick that // lands before the cleanup must not read. This segment pins the unmount, not the predicate. const afterTick = count.calls @@ -363,7 +365,7 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b // Closing that board puts this one back in front: the next tick reads again. win.location.hash = '#project' await tick() - assert.equal(count.calls, afterTick + 5, 'closing the row’s board resumes the poll') + assert.equal(count.calls, afterTick + 4, 'closing the row’s board resumes the poll') // A hidden tab reads nothing either — this is the segment the named predicate is pinned by. const afterResume = count.calls doc.visibilityState = 'hidden' @@ -371,7 +373,7 @@ test('#1814: an open board reads on every tick, stops once a row opens its own b assert.equal(count.calls, afterResume, 'no read in a hidden tab') doc.visibilityState = 'visible' await tick() - assert.equal(count.calls, afterResume + 5, 'a visible tab reads again on the next tick') + assert.equal(count.calls, afterResume + 4, 'a visible tab reads again on the next tick') // Close the board: the timer goes with it, so a tick afterwards reads nothing at all. const afterVisible = count.calls for (const off of cleanups) off()