From 6299e1c11a8c516451fd44a0f0694c2e02d1fd1b Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Fri, 30 Oct 2026 07:56:27 +0800 Subject: [PATCH 1/3] fix(client): read one task row in the Inbox and on the Board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Inbox showed a task that was never claimed as "In progress" and aged it from its creation, so the same rows read "In progress · for 63h" while the Board painted them Not started (0/5). The Inbox now reads the row through the board's own column mapping, so a not-started row is not an In progress card, and a card ages from the moment the row started instead of its creation (#1739). Docs-not-needed: the Inbox and the Board now share the existing task-row mapping; no subsystem contract changes, only the client rendering of it. --- .../1739-task-status-one-source.fixed.md | 1 + core/client.js | 111 +- core/src/client/inbox.js | 17 +- core/test/inbox-task-status.test.js | 86 + patches/ds-bot/0008-client-bundle.patch | 6745 +++++++++++------ patches/ds-bot/files/src/client/inbox.js | 17 +- .../files/test/inbox-task-status.test.js | 86 + 7 files changed, 4731 insertions(+), 2332 deletions(-) create mode 100644 changelog.d/1739-task-status-one-source.fixed.md create mode 100644 core/test/inbox-task-status.test.js create mode 100644 patches/ds-bot/files/test/inbox-task-status.test.js diff --git a/changelog.d/1739-task-status-one-source.fixed.md b/changelog.d/1739-task-status-one-source.fixed.md new file mode 100644 index 000000000..d05d7a602 --- /dev/null +++ b/changelog.d/1739-task-status-one-source.fixed.md @@ -0,0 +1 @@ +- The Inbox no longer shows a task that was never started as "In progress", and no longer ages it from its creation: the Inbox and the Board now read one task row, so the status label and the counter agree and a card's age comes from the moment the row started. diff --git a/core/client.js b/core/client.js index 4ceed1696..af79faf7c 100644 --- a/core/client.js +++ b/core/client.js @@ -7707,6 +7707,56 @@ body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a} @media (prefers-reduced-motion:reduce){.bt-mw-row{animation:none}} `; +// src/shared/task-board.js +var COLUMNS = ["not-started", "in-progress", "done"]; +var PRIORITIES = ["urgent", "high", "normal", "low"]; +var COLUMN_OF = { + open: "not-started", + claimed: "not-started", + working: "in-progress", + blocked: "in-progress", + "result-ready": "in-progress", + accepted: "done", + cancelled: "done", + failed: "done" +}; +function columnOf(task) { + return COLUMN_OF[task?.status] ?? "not-started"; +} +function priorityOf(task) { + return PRIORITIES.includes(task?.priority) ? task.priority : "normal"; +} +var PRIORITY_RANK = { urgent: 0, high: 1, normal: 2, low: 3 }; +function workBucket(task) { + if (task?.status === "open" || task?.status === "claimed" || task?.status === "working") return 0; + if (task?.status === "blocked" || task?.status === "result-ready") return 1; + return 2; +} +function compareForWork(a, b) { + const byBucket = workBucket(a) - workBucket(b); + if (byBucket !== 0) return byBucket; + const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); + if (byPriority !== 0) return byPriority; + const byWorking = (a?.status === "working" ? 0 : 1) - (b?.status === "working" ? 0 : 1); + if (byWorking !== 0) return byWorking; + return (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0); +} +function sameRole(a, b) { + const left = String(a ?? "").trim().toLowerCase(); + const right = String(b ?? "").trim().toLowerCase(); + return left !== "" && left === right; +} +function compareForQueue(a, b) { + const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); + if (byPriority !== 0) return byPriority; + return (a?.createdAt ?? 0) - (b?.createdAt ?? 0); +} +function progressOf(task) { + const value = task?.progress; + if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; + return Math.max(0, Math.min(100, Math.round(value))); +} + // src/client/inbox.js var import_react13 = require("react"); var KEEP_MS = 7 * 864e5; @@ -7975,8 +8025,11 @@ function inboxModel({ roster, work = {}, tasks = [], sessions, workflows = [], n if (fresh(task.updatedAt)) needs.push({ ...card, status: "failed", reason: lastWhy(task) }); } else if (task.status === "result-ready") needs.push({ ...card, status: "result", reason: task.result?.summary ?? "" }); else if (task.status === "blocked") needs.push({ ...card, status: task.waitingFor === "input" ? "input" : "approval", reason: task.inputRequest?.question ?? "" }); - else if (task.status === "open" || task.status === "claimed" || task.status === "working") progress.push({ ...card, status: "task", since: task.createdAt }); - else if ((task.status === "accepted" || task.status === "cancelled") && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "accepted" ? "done" : "cancelled" }); + else { + 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" }); + } } for (const graph of Array.isArray(workflows) ? workflows : []) { if (graph === null || typeof graph !== "object") continue; @@ -8027,6 +8080,10 @@ function inboxModel({ roster, work = {}, tasks = [], sessions, workflows = [], n return { needs: needs.sort(newest), progress: progress.sort(newest), done: done.sort(newest).slice(0, DONE_MAX) }; } var needsCount = (roster, work, tasks, now, workflows = []) => inboxModel({ roster, work, tasks, now, workflows }).needs.length; +function startedAtOf(task) { + const started = (task.actors ?? []).filter((actor) => actor.status === "working" && typeof actor.at === "number"); + return started.length ? started.at(-1).at : task.updatedAt; +} function visibleNeeds(model, answered) { return { ...model, needs: model.needs.filter((card) => !answered?.[card.id]) }; } @@ -8406,56 +8463,6 @@ var INBOX_CSS = ` .bt-inbox-actions .bt-send{height:28px;font-size:12px} `; -// src/shared/task-board.js -var COLUMNS = ["not-started", "in-progress", "done"]; -var PRIORITIES = ["urgent", "high", "normal", "low"]; -var COLUMN_OF = { - open: "not-started", - claimed: "not-started", - working: "in-progress", - blocked: "in-progress", - "result-ready": "in-progress", - accepted: "done", - cancelled: "done", - failed: "done" -}; -function columnOf(task) { - return COLUMN_OF[task?.status] ?? "not-started"; -} -function priorityOf(task) { - return PRIORITIES.includes(task?.priority) ? task.priority : "normal"; -} -var PRIORITY_RANK = { urgent: 0, high: 1, normal: 2, low: 3 }; -function workBucket(task) { - if (task?.status === "open" || task?.status === "claimed" || task?.status === "working") return 0; - if (task?.status === "blocked" || task?.status === "result-ready") return 1; - return 2; -} -function compareForWork(a, b) { - const byBucket = workBucket(a) - workBucket(b); - if (byBucket !== 0) return byBucket; - const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); - if (byPriority !== 0) return byPriority; - const byWorking = (a?.status === "working" ? 0 : 1) - (b?.status === "working" ? 0 : 1); - if (byWorking !== 0) return byWorking; - return (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0); -} -function sameRole(a, b) { - const left = String(a ?? "").trim().toLowerCase(); - const right = String(b ?? "").trim().toLowerCase(); - return left !== "" && left === right; -} -function compareForQueue(a, b) { - const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); - if (byPriority !== 0) return byPriority; - return (a?.createdAt ?? 0) - (b?.createdAt ?? 0); -} -function progressOf(task) { - const value = task?.progress; - if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; - return Math.max(0, Math.min(100, Math.round(value))); -} - // src/shared/board-card.js function taskCardKey(id) { const text2 = String(id ?? "").trim(); diff --git a/core/src/client/inbox.js b/core/src/client/inbox.js index 68666c405..df29c687b 100644 --- a/core/src/client/inbox.js +++ b/core/src/client/inbox.js @@ -17,6 +17,7 @@ import { alwaysChoices } from './member-status.js' import { botNames, currentPart, previewOf, relativeShort } from './text.js' import { hostText, t } from './i18n.js' import { failureSentence } from '../shared/failure-copy.js' +import { columnOf } from '../shared/task-board.js' // Done (and a failed task) ages out after 7 days. export const KEEP_MS = 7 * 86_400_000 @@ -292,8 +293,13 @@ export function inboxModel({ roster, work = {}, tasks = [], sessions, workflows if (task.status === 'failed') { if (fresh(task.updatedAt)) needs.push({ ...card, status: 'failed', reason: lastWhy(task) }) } else if (task.status === 'result-ready') needs.push({ ...card, status: 'result', reason: task.result?.summary ?? '' }) else if (task.status === 'blocked') needs.push({ ...card, status: task.waitingFor === 'input' ? 'input' : 'approval', reason: task.inputRequest?.question ?? '' }) - else if (task.status === 'open' || task.status === 'claimed' || task.status === 'working') progress.push({ ...card, status: 'task', since: task.createdAt }) - else if ((task.status === 'accepted' || task.status === 'cancelled') && fresh(task.updatedAt)) done.push({ ...card, status: task.status === 'accepted' ? 'done' : 'cancelled' }) + else { + // One mapping with the board (#1739): the column is the task row's own status, so the + // Inbox never gives a row a state the board does not give it. + 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' }) + } } // A workflow run that failed (#1706). The plugin keeps that failure in its own // state, so the Inbox reads it from the same pack the Workflows page gets. One @@ -358,6 +364,13 @@ export const needsCount = (roster, work, tasks, now, workflows = []) => inboxMod // Cards the owner already answered leave the Needs you list. The dialog keeps that // set until the next roster poll drops the underlying record. +// A task card's age is the row's own clock (#1739): the moment the row entered working, +// never the wall clock and never its creation, so a row that never started carries no age. +function startedAtOf(task) { + const started = (task.actors ?? []).filter(actor => actor.status === 'working' && typeof actor.at === 'number') + return started.length ? started.at(-1).at : task.updatedAt +} + export function visibleNeeds(model, answered) { return { ...model, needs: model.needs.filter(card => !answered?.[card.id]) } } diff --git a/core/test/inbox-task-status.test.js b/core/test/inbox-task-status.test.js new file mode 100644 index 000000000..132d7ce24 --- /dev/null +++ b/core/test/inbox-task-status.test.js @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' +import { columnOf } from '../src/shared/task-board.js' + +// #1739: the Inbox and the Board must read one task row the same way. The row's status is the +// only source; neither view derives a state from session or turn recency, and a card's age is +// the row's own started-at, never its creation — a row that never started has no age. +const bundle = await build({ + entryPoints: [fileURLToPath(new URL('../src/client/inbox.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout: () => 0, clearTimeout: () => {}, setInterval: () => 0, clearInterval: () => {}, Date, Promise, + require: (name) => { + const platform = { + react: { + createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }), + useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}], + useEffect: () => {}, useLayoutEffect: () => {}, useRef: current => ({ current }), useCallback: fn => fn, useMemo: fn => fn(), Fragment: 'fragment', + }, + 'react-dom': {}, + '@deepseek-ai/dsh-client-ui-primitives': {}, + } + if (!(name in platform)) throw new Error(`no platform module ${name}`) + return platform[name] + }, +}) +const { inboxModel } = module.exports + +const NOW = Date.UTC(2026, 10, 29, 23, 0, 0) +const HOUR = 3_600_000 +const roster = { + self: { memberId: 'm1' }, + bots: [{ id: 'b1', name: 'Chief', ownerId: 'm1', sessionId: 's1' }], + rooms: [], +} +roster.byId = Object.fromEntries(roster.bots.map(bot => [bot.id, bot])) +roster.roomsById = {} +const row = (id, status, extra = {}) => ({ + id, status, title: `Task ${id}`, ownerBotId: 'b1', requesterId: 'm1', origin: {}, + createdAt: NOW - 63 * HOUR, updatedAt: NOW - 63 * HOUR, actors: [], ...extra, +}) +// The reported set: five rows that were never claimed. The board paints them Not started (0/5). +const neverStarted = ['t1', 't2', 't3', 't4', 't5'].map(id => row(id, 'open')) +const claimed = row('t6', 'claimed', { actors: [{ status: 'claimed', at: NOW - 63 * HOUR }] }) +const working = row('t7', 'working', { + createdAt: NOW - 70 * HOUR, + updatedAt: NOW - 2 * HOUR, + actors: [{ status: 'claimed', at: NOW - 70 * HOUR }, { status: 'working', at: NOW - 63 * HOUR }], +}) +const workingNoLog = row('t8', 'working', { createdAt: NOW - 70 * HOUR, updatedAt: NOW - 3 * HOUR }) +const accepted = row('t9', 'accepted', { updatedAt: NOW - HOUR }) +const tasks = [...neverStarted, claimed, working, workingNoLog, accepted] +const model = inboxModel({ roster, work: {}, tasks, sessions: { byId: {} }, now: NOW }) +const ids = list => Array.from(list, card => String(card.id)) + +test('a row the board paints Not started is not an In progress card in the Inbox (#1739)', () => { + // The reported defect: Board 0/5 Not started while the Inbox showed the same five rows as + // In progress for 63h. + assert.deepEqual(ids(model.progress), ['task:t7', 'task:t8']) + for (const task of [...neverStarted, claimed, accepted]) { + assert.ok(!ids(model.progress).includes(`task:${task.id}`), `${task.id} (${task.status}) must not be In progress`) + } +}) + +test('both views read the same row the same way: In progress is exactly the board in-progress column', () => { + const boardInProgress = tasks.filter(task => columnOf(task) === 'in-progress').map(task => `task:${task.id}`) + assert.deepEqual(ids(model.progress), boardInProgress) + const boardNotStarted = tasks.filter(task => columnOf(task) === 'not-started') + assert.equal(boardNotStarted.length, 6) + // The Inbox shows no card at all for those rows, so it can show no age for them either. + assert.deepEqual(ids(model.progress).filter(id => boardNotStarted.some(task => `task:${task.id}` === id)), []) +}) + +test('a card age comes from the row clock: the moment it started, never its creation', () => { + const card = model.progress.find(entry => entry.id === 'task:t7') + assert.equal(card.since, NOW - 63 * HOUR) + const fallback = model.progress.find(entry => entry.id === 'task:t8') + assert.equal(fallback.since, NOW - 3 * HOUR, 'a working row with no transition log ages from its own updatedAt') + for (const entry of model.progress) assert.ok(typeof entry.since === 'number' && entry.since <= NOW) +}) diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 68343aacb..f64d2f290 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1706,6 +1706,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 = [ @@ -1753,11 +1758,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))); @@ -1995,16 +1998,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 +2631,982 @@ +@@ -1624,324 +2631,1314 @@ 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; @@ -2019,28 +2071,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) { @@ -2053,7 +2161,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); + } @@ -2066,20 +2193,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; @@ -2096,7 +2278,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"; @@ -2113,7 +2295,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); @@ -2131,7 +2314,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)); @@ -2211,17 +2396,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; +} @@ -2314,8 +2504,109 @@ diff --git a/client.js b/client.js + return (0, import_react.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); + }) + ); -+} -+ + } + +-// src/client/styles.js +-var SHELL_CSS = ` +-body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)} +-body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)} +-${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")} +-${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")} +-.bt-mark{display:inline-block;flex:none;position:relative;line-height:0} +-.bt-mark svg{width:100%;height:100%;overflow:visible} +-.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} +-.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} +-.bt-mark .bt-b{transform-origin:50% 100%} +-.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} +-/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a +- mark only for its state; normal, the default, adds petting, pokes and eyes that follow +- the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */ +-body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)} +-body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite} +-body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop} +-body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway} +-body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch} +-body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite} +-@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}} +-@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}} +-@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}} +-@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} +-body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} +-body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} +-@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} +-/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its +- size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes +- closed in a happy arc, and springs back when the pointer leaves. */ +-:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%} +-.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out} +-.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in} +-.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out} +-body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} +-body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none} +-body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s} +-@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}} +-.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out} +-.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out} +-body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none} +-body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none} +-.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite} +-.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite} +-.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite} +-.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite} +-.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite} +-.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%} +-.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite} +-.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite} +-.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite} +-.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%} +-.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite} +-/* A question for you hops twice when it arrives and the badge keeps it in view after; +- a lively mark keeps hopping. */ +-.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} +-.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite} +-.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} +-body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite} +-.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1} +-.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%} +-@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}} +-.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important} +-.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important} +-.bt-mark[data-poke]{cursor:pointer} +-body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important} +-body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} +-@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}} +-@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}} +-@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}} +-@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}} +-@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}} +-@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}} +-@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}} +-@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}} +-@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}} +-@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}} +-@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}} +-@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}} +-@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}} +-/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */ +-@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}} +-@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}} +-@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}} +-.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))} +-.bt-badge-admin svg{width:7px;height:7px} +-.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none} +-.bt-badge-main svg{width:100%;height:100%;overflow:visible} +-.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke} +-.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px} +-.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0} +-.bt-cluster>*{position:absolute} +-.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums} +-.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)} +-.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0} +-.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)} +-.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important} +- +// src/client/room-invite-preview.js +var INVITE_PREVIEW_NAME_MAX = 60; +function stripInviteMarkers(text2) { @@ -2976,112 +3267,236 @@ diff --git a/client.js b/client.js + +// 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 +3639,12 @@ - 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 h49 = 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) * h49; + value.x += value.v * h49; - } - if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { - value.x = value.to; -@@ -1699,10 +3679,10 @@ - } - 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 +3716,7 @@ - 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 +3730,7 @@ - } - 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 +3738,16 @@ - } - 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 +3768,8 @@ - }, [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 +3797,8 @@ - }); - } - 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 +3818,7 @@ - .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 +3837,14 @@ - @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} @@ -3090,10 +3505,80 @@ 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 +3922,23 @@ ++@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} @@ -3542,140 +4027,61 @@ diff --git a/client.js b/client.js document.head.appendChild(style); return style; } -@@ -2807,9 +4945,9 @@ +@@ -2805,1975 +4943,4192 @@ + visit(sheet.cssRules); + } catch { } - return names; - } --function compileFragmentSelectors(text, names) { +- return names; ++ 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 +4956,7 @@ - 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 +4970,7 @@ - } - 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 +4993,204 @@ - }; - } - --// 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; ++ 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) { @@ -3692,28 +4098,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 BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { -- const settled = useSettled(state); -- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); -- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, -- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), -- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, -- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, -- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, -- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null -- ); --} --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; ++} +function ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -3724,43 +4109,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 = []; @@ -3865,135 +4217,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 +5202,7 @@ - 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 +5210,14 @@ - 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 +5229,48 @@ - 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", @@ -4017,670 +4350,950 @@ diff --git a/client.js b/client.js + (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"); ++ ++// 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 +5290,10 @@ - }, [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)(); ++ 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_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 +5311,7 @@ - }; - 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 +5330,7 @@ - }) : 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 +5340,36 @@ - "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 +5379,24 @@ - 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 +5406,29 @@ - const prompt = question?.question ?? ""; - const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; - if (chosen.length === 0) { -- return (0, import_react4.createElement)( ++ ) : null, ++ foot ++ ); + } +-function compileFragmentSelectors(text, names) { +- const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { +- if (!compiled.has(fragment)) { +- const exact = []; +- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); +- } +- return compiled.get(fragment); +- }); ++function 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 +5436,8 @@ - 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 installCompiledStyles(text, tag) { +- const style = installStyles("", tag); +- const cache = /* @__PURE__ */ new WeakMap(); +- const render = () => { +- const names = /* @__PURE__ */ new Set(); +- for (const sheet of document.styleSheets) { +- if (sheet.ownerNode?.dataset?.dshBot) continue; +- let list = cache.get(sheet); +- if (!list) { +- list = sheetClassNames(sheet); +- cache.set(sheet, list); +- } +- for (const name of list) names.add(name); ++function 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 +5449,10 @@ ++ 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 }); +- 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(); ++ pending.answer({ answers: next }); }; -- 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 }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); +- +-// src/client/mark.js +-var import_react2 = require("react"); +-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +-function useGaze(ref, enabled) { +- (0, import_react2.useEffect)(() => { +- if (!enabled) return void 0; +- let frame = 0; +- let point = null; +- const place = () => { +- frame = 0; +- const node = ref.current; +- if (!node || !point) return; +- const box = node.getBoundingClientRect(); +- const clamp = (value) => Math.max(-1, Math.min(1, value)); +- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); +- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); +- }; +- const onMove = (event) => { +- point = { x: event.clientX, y: event.clientY }; +- if (!frame) frame = requestAnimationFrame(place); +- }; +- 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 ++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 +5462,10 @@ - setAnswered(question.id); - void actions.dismissQuestion(sessionId); ++ 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)); }; -- return (0, import_react4.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) } })); +-} +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) ++ const 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 +5473,16 @@ - 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 AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); ++function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const 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 MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); ++function AnsweredCell({ node }) { ++ const data = node.data ?? {}; ++ const questions = Array.isArray(data.questions) ? data.questions : []; ++ 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, { ++ } ++ 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 +5490,37 @@ ++ key: question.id, ++ question, ++ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } ++ }))); } - - // src/client/secret-card.js --var import_react5 = require("react"); +-function useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; ++ ++// src/client/secret-card.js +var import_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 +5545,42 @@ - }); ++ 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?.(); + } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++ }); } - function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null ++function ScopeSwitch({ value, onChange, bot }) { + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_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)( +-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}`); +- } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) ++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)(); +- +-// src/client/icons.js +-var import_react3 = require("react"); +-function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++function SecretCard({ bot, request, roster, actions }) { + const [value, setValue] = (0, import_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 +5596,73 @@ - if (!busy) void run(actions.secretAllow(request.id)); - }; - const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( ++ const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); ++ const run = (promise) => { ++ setBusy(true); ++ setError(""); ++ return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); ++ }; ++ const cancel = () => run(actions.secretCancel(request.id)); ++ const long = value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); ++ }; ++ const allow = () => { ++ if (!busy) void run(actions.secretAllow(request.id)); ++ }; ++ const filling = request.kind === "fill" || replacing; + const warn = insecure() ? (0, import_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 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 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 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 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 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 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 +5675,46 @@ - 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 WaveIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) +- ); ++function 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 ShareIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +var 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 CloseIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); +-var lineIcon = (paths) => function LineIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) ++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 +5729,66 @@ - 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 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)(() => { +-}; +-var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; +-function CloudDownloadIcon({ done = false }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: CLOUD }), +- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) ++} ++function 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)(""); +-var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); +-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); +-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); +-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); +-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); +-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); +-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); +-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); +-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); +-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); +-var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); +-var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); +-function PinMarkIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( +- "g", +- { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) ++function 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 +5799,43 @@ - } - }); - }; -- 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); +-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 }; ++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]"), -+ (0, import_react7.createElement)("code", null, "[secret:NAME]"), - 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 }) }, -- 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 ++ 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_react7.createElement)( ++ 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 +5902,144 @@ - @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/questions.js +-var import_react4 = require("react"); +-var keyLetter = (index) => String.fromCharCode(65 + index); +-var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); +-var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; +-function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); +- pick2.current = onPick; +- (0, import_react4.useEffect)(() => { +- if (count === 0) return void 0; +- const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); +- } +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -4693,8 +5306,48 @@ diff --git a/client.js b/client.js + person: local ? seatOwnerName(p.ownerName, p.handle) || cleanPeerName(roster.self?.displayName, "") : ownerNameOf(p), + bot: local ? local.name : remoteBotName(p, "Bot"), + mine: Boolean(local) -+ }; -+ }; + }; +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); +-} +-function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); +- const options = optionsOf(question); +- const multi = question.multiSelect === true; +- const custom = own.trim(); +- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; +- const choose = (label) => { +- if (multi) { +- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +- return; +- } +- onAnswer({ selected: [label] }); +- }; +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); + }; +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); + const seats = records.filter((p) => p.leftSeq === void 0).map(seat); + for (const id of room2.members ?? []) { + const bot = roster.byId[id]; @@ -4731,9 +5384,42 @@ diff --git a/client.js b/client.js + if (known) { + known.seats.push(seat); + continue; -+ } + } +- }) : 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) +- )); + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); -+ } + } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; +} +function facePersonLabel(person) { @@ -4790,91 +5476,166 @@ diff --git a/client.js b/client.js + if (!waiting) { + const failed = held.status === "failed"; + return (0, import_react8.createElement)( -+ "div", + "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) + { className: "bt-held-note", "data-state": held.status, title: failed && held.error ? held.error : void 0 }, + (0, import_react8.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")), + // #968: the terminal answer comes from one predicate, not from `status === 'failed'` here and a + // different field elsewhere. A failure that can never succeed draws no button on any surface. + failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null, + error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null -+ ); -+ } + ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); + } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( + const plain = (0, import_react8.createElement)( + "div", + { className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") }, + (0, import_react8.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")), + (0, import_react8.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })), + (0, import_react8.createElement)( -+ "div", + "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) + { className: "bt-flybot-approval-actions" }, + (0, import_react8.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")), + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) -+ ), + ), +- 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 + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null + ); + 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 SettledQuestion({ question, answer }) { +- const options = optionsOf(question); +- const prompt = question?.question ?? ""; +- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; +- if (chosen.length === 0) { +- return (0, import_react4.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) +- ) +- ); +- } +- return (0, import_react4.createElement)( + - // src/client/turn-reasoning.js - function turnReasoning(steps, current) { - const members = steps.length ? steps : [current]; -@@ -3767,7 +6051,7 @@ - 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 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 +6064,29 @@ - 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) => { +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( ++ { className: "bt-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)( +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) ++ { key: index, className: "bt-msg" }, + (0, import_react9.createElement)( - "div", - { 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 +6097,22 @@ - }) ++ key: emoji, ++ type: "button", ++ className: "bt-react", ++ "aria-label": t("Remove reaction {emoji}", { emoji }), ++ onClick: () => actions.toggleReaction(msgKey, index, emoji) ++ }, emoji))) : null + ); +- })) ++ }) ); } +-function QuestionCard({ matched }) { +- const pending = matched; +- const questions = pending.questions ?? []; +- const [index, setIndex] = (0, import_react4.useState)(0); +- const [answers, setAnswers] = (0, import_react4.useState)([]); +- const question = questions[index]; +- if (question === void 0) return null; +- const onAnswer = (answer) => { +- const next = [...answers, { id: question.id, ...answer }]; +- if (index + 1 < questions.length) { +- setAnswers(next); +- setIndex(index + 1); +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)?$/); @@ -4887,85 +5648,110 @@ diff --git a/client.js b/client.js + } + return false; +} - function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { ++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 +6132,12 @@ - }; - }, [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); ++ if (!open) return void 0; ++ const onDown = (event) => { ++ if (refs.some((ref) => ref.current?.contains(event.target))) return; ++ close(); ++ }; ++ const onKey = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ close(); ++ } ++ }; ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ }; ++ }, [open]); ++} +function MessageTools({ text: 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 +6151,36 @@ - setPanel(kind); ++ const close = () => setPanel(null); ++ useDismiss(panel !== null, close, [tools, pop]); ++ const open = (kind, event) => { ++ if (panel === kind) { ++ close(); + return; + } +- pending.answer({ answers: next }); ++ const rect = event.currentTarget.getBoundingClientRect(); ++ const below = rect.bottom + 220 < window.innerHeight; ++ setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); ++ setPanel(kind); }; - const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +-} +-function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); +- if (answered === question.id) return null; +- const onAnswer = (answer) => { +- setAnswered(question.id); +- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); ++ const copy2 = () => { + void navigator.clipboard?.writeText(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" })); +- const onDismiss = () => { +- setAnswered(question.id); +- void actions.dismissQuestion(sessionId); ++ 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 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: () => { +- return (0, import_react4.createElement)( + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { - close(); - run(); -- } }, (0, import_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)( +- { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ++ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, 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: () => { ++ "div", ++ { 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 +6188,40 @@ - ), document.body) : null ++ "div", ++ { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, ++ item(t("Copy"), CopyIcon, copy2), ++ item(t("Download"), DownloadIcon, download2) ++ ), document.body) : null ); } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( +-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 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; +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -4977,9 +5763,69 @@ diff --git a/client.js b/client.js + for (const part of graphemes(line2)) { + if (kept.length + part.length > PEEK_MAX - CUT.length) break; + kept += part; -+ } + } +- const 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: [] } +- }))); +-} +- +-// 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 ")); + return kept + CUT; -+}; + }; +-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; +-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +- } +var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length }); +var peekLine = (text2) => { + const lines = text2.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== ""); @@ -4990,10 +5836,8 @@ 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, @@ -5002,19 +5846,18 @@ diff --git a/client.js b/client.js + peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null + ), + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) - ); - } - function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,19 +6229,33 @@ - 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 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; @@ -5026,45 +5869,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 +6263,14 @@ - 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 +6280,230 @@ - 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 + ); @@ -5087,47 +5926,35 @@ 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)( ++ "div", ++ { 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)); ++ ) ++ ); ++ } ++ 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) { @@ -5139,8 +5966,11 @@ diff --git a/client.js b/client.js + if (bot !== void 0 && String(item.from ?? "").trim() !== "" && !sameName(item.from, bot.name)) return false; + const held = heldForm(item.text); + return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held)); -+ }); -+} + }); + } +-function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( +var HELD_SENDING_MS = 6e4; +function heldLineState(room2, { seq, botId, text: text2 } = {}, now = Date.now()) { + if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0; @@ -5164,12 +5994,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)) } }, @@ -5178,12 +6006,27 @@ diff --git a/client.js b/client.js + ); + return (0, import_react9.createElement)( "div", - { className: "bt-group-msg" }, -- (0, import_react6.createElement)( +- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { +- key: id, +- type: "button", +- role: "radio", +- "aria-checked": value === id, +- className: "bt-secret-seg", +- onClick: () => onChange(id) +- }, text)) ++ { className: "bt-group-msg" }, + name, + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) -+ ); -+} + ); + } +-function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -5224,7 +6067,10 @@ diff --git a/client.js b/client.js + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + if (person) { + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") + { className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle }, + // One short header: the person, and the Bot that carried the line only when it is known (#275). + (0, import_react9.createElement)( @@ -5244,18 +6090,26 @@ diff --git a/client.js b/client.js + { 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 }) +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(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 SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); +- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); +- const run = (promise) => { +- setBusy(true); +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; @@ -5275,7 +6129,16 @@ diff --git a/client.js b/client.js + const [error, setError] = (0, import_react9.useState)(""); + const download2 = async (file) => { + setBusy(file.id); -+ setError(""); + setError(""); +- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); +- }; +- const cancel = () => run(actions.secretCancel(request.id)); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); +- }; +- const allow = () => { +- if (!busy) void run(actions.secretAllow(request.id)); + try { + const got = await call(`flybot/${plugin}/attachment`, { id: file.id, botId }); + const value = got?.value ?? got; @@ -5289,9 +6152,44 @@ diff --git a/client.js b/client.js + } finally { + setBusy(""); + } -+ }; + }; +- const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( + return (0, import_react9.createElement)( -+ "div", + "div", +- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { +- bot, +- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), +- detail: request.purpose, +- name: request.name, +- onClose: busy ? void 0 : cancel +- }), +- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, +- filling ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-body" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), +- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null +- ) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-note", id: noteId }, +- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), +- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), +- ".", +- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" +- ), +- warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-foot" }, +- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), +- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) +- ) + { className: "bt-file-chips" }, + files.map((file) => (0, import_react9.createElement)( + "button", @@ -5301,229 +6199,254 @@ diff --git a/client.js b/client.js + 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 SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( ++function GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_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-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( ++ { 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 +6514,14 @@ - 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 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) ++ "div", ++ { className: "bt-event" }, + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) - ); - } - function 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 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 +6530,11 @@ - 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 +6550,19 @@ - if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); - }); - }; -- (0, import_react6.useEffect)(() => { ++ 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_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 +6578,35 @@ - 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)( ++ "div", ++ { 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-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)) +- ) ++ { 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 }); ++ ), ++ 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 +6614,7 @@ - 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 +6625,7 @@ - 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 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 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 +6644,31 @@ - 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 +6682,7 @@ - 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 +6691,24 @@ - } - 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 +6716,67 @@ - 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; - } +-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 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([ @@ -5576,17 +6499,153 @@ diff --git a/client.js b/client.js + if (isHiddenNotice(block)) continue; + if (block?.type === "tool-removal" && block.toolName) parts.push(`Tool removed: ${block.toolName}`); + else if (block?.type === "tool-addition" && block.toolName) parts.push(`Tool added: ${block.toolName}`); -+ } + } +- return null; + 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 +6805,19 @@ - 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 SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { +- const roster = useRoster((value) => value); +- const shown = useCards?.((state) => state[sessionId]) ?? NONE; +- const loading = useSession?.(historyLoading) === true; +- const bot = roster.byId[sessionId]; +- if (!bot || currentPart(bot) !== sessionId) return null; +- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); +- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); +- if (waiting.length === 0) return null; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); ++var 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 ScopeEditor({ secret, roster, onSave }) { +- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); +- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); +- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( +- "label", +- { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text +- )) +- ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { +- key: bot.id, +- type: "button", +- className: "bt-secret-bot", +- "aria-pressed": picked.includes(bot.id), +- onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) +- ) +- ); ++function 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 ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!long) return; +- void Promise.resolve(onSave(value)).then((ok) => { +- if (ok) { +- setValue(""); +- setReplaced(true); +- } +- }); +- }; +- const edit = (next) => { +- setValue(next); +- setReplaced(false); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-inline" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) +- ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null +- ); ++function 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 SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) +- ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null +- ); ++var 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 ?? {}) { @@ -5594,177 +6653,368 @@ diff --git a/client.js b/client.js + if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true; + } + return false; -+} + } +-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")) +- ) +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 +6831,15 @@ - 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 SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; +-} +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +-`; +- +-// src/client/turn-reasoning.js +-function turnReasoning(steps, current) { +- const members = steps.length ? steps : [current]; +- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; +- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); ++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 +6901,22 @@ - } - function workPhrase(tool) { - if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +-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)( ++var PHRASES = { ++ thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], ++ working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] ++}; ++var TOOL_PHRASES = [ ++ [/web_?search|search_?web/i, "Searching online"], ++ [/fetch|browse|browser|web/i, "Reading a page"], ++ [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], ++ [/^(read|view|cat)\b|read_/i, "Opening a file"], ++ [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], ++ [/write|edit|patch|apply|replace/i, "Writing changes"], ++ [/image|photo|draw/i, "Working on an image"] ++]; ++var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; ++function toolActivity(name, arg, findBot, self) { ++ switch (name) { ++ case "ask_user": ++ return null; ++ case "request_secret": ++ return null; ++ case "list_secrets": ++ return { label: t("Checking the keys"), state: "searching" }; ++ case "ask_user_question": ++ return { label: t("Waiting for your answer"), state: "alert" }; ++ case "group_relay": ++ return { relay: true, state: "orbit" }; ++ case "message_bot": { ++ const target = findBot(arg("to")); ++ return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; ++ } ++ case "stop_bot": { ++ const target = findBot(arg("bot")); ++ return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; ++ } ++ case "create_bot": ++ return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; ++ case "update_bot": { ++ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); ++ return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; ++ } ++ case "list_bots": ++ return { label: t("Checking the team"), state: "searching" }; ++ case "read_group_chat": ++ return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; ++ case "read_own_chat": ++ return { label: t("Looking back through the chat"), state: "searching" }; ++ case "remember": ++ return { label: t("Saving to memory"), state: "working" }; ++ case "forget": ++ return { label: t("Updating memory"), state: "working" }; ++ case "recall": ++ return { label: t("Checking memory"), state: "searching" }; ++ default: ++ if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; ++ const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; ++ return { label: phrase ? t(phrase) : null, state: toolState(name) }; ++ } ++} ++function workPhrase(tool) { ++ if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); +} +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 +6925,10 @@ - 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 +6946,89 @@ - 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-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( ++ { className: "bt-group-replies" }, ++ replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; + 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" }) -- )); +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( -+ "div", + "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 }) + { className: "bt-ububble" }, + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" }) -+ ), + ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); - } - 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") { @@ -5787,148 +7037,133 @@ diff --git a/client.js b/client.js }) ); } -@@ -4435,52 +7037,52 @@ - function callTree(root) { - const calls = []; - const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); +- } ++var 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 +7094,90 @@ - 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; - } - 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 })); ++ } catch { ++ parsed = null; ++ } ++ const answer = parsed?.answers?.[0]; ++ const question = argValue("questions")?.[0]; ++ if (!answer) return null; + return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); - } - case "read_group_chat": { - if (!done) return null; - const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_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; - } - } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { ++ default: ++ return 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)))); +} @@ -5957,21 +7192,19 @@ diff --git a/client.js b/client.js + if (typeof root.matches === "function" && root.matches("[data-chat-running]")) return root; + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); - } - - // src/client/activity-row.js --var import_react7 = require("react"); ++} ++ ++// 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 +7187,69 @@ - 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]; @@ -6019,61 +7252,245 @@ diff --git a/client.js b/client.js + const check = () => { + frame = 0; + if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true); -+ }; + }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); + }); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + check(); -+ return () => { + return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); + cancelAnimationFrame(frame); + observer.disconnect(); -+ }; + }; +- }, [open]); + }, [remote, sending]); + return replied; -+} + } +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react10.useState)(null); + (0, import_react10.useEffect)(() => { - if (!active) { - setHost(null); - return void 0; -@@ -4569,7 +7257,7 @@ - let frame = 0; - const find = () => { - frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); ++ if (!active) { ++ setHost(null); ++ return void 0; + } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); ++ let frame = 0; ++ const find = () => { ++ frame = 0; + const node = findHost(where); - setHost((prev) => prev === node ? prev : node); - }; - const observer = new MutationObserver(() => { -@@ -4581,13 +7269,13 @@ - cancelAnimationFrame(frame); - observer.disconnect(); - }; -- }, [active]); ++ setHost((prev) => prev === node ? prev : node); ++ }; ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(find); + }); +- }; +- const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); +- link.click(); +- setTimeout(() => URL.revokeObjectURL(url), 1e3); +- }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { +- close(); +- run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( +- "div", +- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, +- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { +- close(); +- react(emoji); +- } }, emoji)) +- ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, +- item(t("Copy"), CopyIcon, copy2), +- item(t("Download"), DownloadIcon, download2) +- ), document.body) : null +- ); +-} +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +- "details", +- { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +- ); +-} +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; +- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; +- }, [fileMentions, openFile, seq, tail, turn]); ++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); ++ find(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; + }, [active, where]); - return host; ++ return host; } - function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); +-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- const running = data.status === "running"; +- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); +- if (hidden) return null; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; +- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true ++function useRowEntrances(replacing) { + const check = (0, import_react10.useRef)(replacing); - check.current = replacing; -- (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 +7326,31 @@ - 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 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; + }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +- ); +-} +-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { +- if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, +- (0, import_react6.createElement)("span", null, prefix), +- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) +- ) +- ); +- } +- const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( +- "button", +- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- bot.name +- )); +- }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); ++ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); ++ for (const item of fresh) seen.add(item); ++ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; ++ for (const item of fresh) { ++ if (items.indexOf(item) < last) continue; ++ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); ++ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; ++ item.addEventListener("animationend", settle); ++ } ++ }; ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(scan); ++ }); ++ scan(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; ++ }, []); + } +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) +- ); +- return (0, import_react6.createElement)( ++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; @@ -6084,49 +7501,37 @@ 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 +7361,31 @@ - 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 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); @@ -6137,35 +7542,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 +7394,37 @@ - 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); @@ -6173,52 +7572,200 @@ 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)( +- { className: "bt-group-msg" }, +- (0, import_react6.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" }, -- (0, import_react7.createElement)( +- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, +- bot.name, +- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null +- ), +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) +- ); +-} +-function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null +- ); +-} +-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- const target = other && other.id !== self?.id ? other : self; +- if (!target) return null; +- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) +- ); ++ { 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 +7440,1687 @@ - return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ ) ++ ) ++ ), host); } - +-function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); ++ ++// src/client/dsh-offer.js ++var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); ++var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; ++var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); ++function showsDshRow(dsh, app) { ++ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; ++ if (dsh.kind === "npm") return true; ++ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); + } +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; +- } +- return null; ++ +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; +var onEscapeKey = (event) => { + if (event.key === "Escape") escapeLayers.at(-1)?.current(); -+}; + }; +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +- }); +- }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); +function useEscape(onClose) { + const ref = (0, import_react11.useRef)(onClose); + ref.current = onClose; + (0, import_react11.useEffect)(() => { + if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); + escapeLayers.push(ref); -+ return () => { + return () => { +- alive.current = false; + escapeLayers.splice(escapeLayers.indexOf(ref), 1); + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); -+ }; + }; +- }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { +- const pending = restore.current; +- restore.current = null; +- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; +- }, [view.items]); +- (0, import_react6.useEffect)(() => { +- const node = top.current; +- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; +- const observer = new IntersectionObserver((entries2) => { +- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); +- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); +- observer.observe(node); +- return () => observer.disconnect(); +- }, [view.cursor, view.loading, view.failed]); +- const selfId = roster.byId[sessionId]?.id; +- const rows = []; +- for (const item of view.items) { +- const last = rows.at(-1); +- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); +- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); +- } +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( +- "div", +- { ref: top, className: "bt-earlier-top" }, +- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null +- ), +- rows.map((row, index) => { +- const key = rows.length - index; +- switch (row.kind) { +- case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null +- )); +- case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); +- case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); +- case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); +- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; +- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { +- if (selfId && other.id) actions.openExchange(selfId, other.id); +- }, whole: true }); +- } +- default: +- return null; +- } +- }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) +- ); + }, []); -+} + } +-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) : ""; +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -6248,12 +7795,74 @@ diff --git a/client.js b/client.js + const doing = workIn(room2, work, id); + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; -+ }); -+} + }); +- const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; +- function triggerBody() { +- if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; +- if (source.role === "secret" || deferred) return null; +- if (source.role === "post") { +- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; +- const room2 = roster.roomsById[sessionId]; +- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); +- } +- if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); +- } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); +- const run = relayed ? commRunOf( +- order.split(",").filter(Boolean).map(Number), +- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +- turn, +- hiddenTurns +- ) : null; +- if (run === "folded") return null; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); +- const bots = [sender]; +- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { +- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); +- } +- return (0, import_react6.createElement)(EventLine, { +- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), +- bots, +- onOpen: (bot) => actions.openExchange(selfId, bot.id), +- whole: true +- }); +- } +- if (source.kind === "schedule") { +- const prompts = reminderPrompts(contentText(data.content)); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); +- } +- if (source.kind === "user-question-reply") return null; +- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); +- } + } +-function NewDivider() { +- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); -+} + } +-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- const own = state[sessionId]?.[turn]; +- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; +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); @@ -6264,8 +7873,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 fresh ? (0, import_react6.createElement)(NewDivider) : null; + } +-function Nothing() { +- return null; +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -6281,7 +7893,17 @@ diff --git a/client.js b/client.js + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) + ); -+} + } +-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}`; +var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +function WorkRow({ bot, tool, step, label, at, now, onOpen }) { + const text2 = `${label} · ${workText(tool, step)}`; @@ -6293,7 +7915,14 @@ diff --git a/client.js b/client.js + (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 + ); -+} + } +-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 SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -6310,7 +7939,15 @@ diff --git a/client.js b/client.js + ), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null + ); -+} + } +-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 MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -6344,7 +7981,31 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); -+} + } +-var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { +- const turn = location2.turn; +- const time = location2.end.time; +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "end", time); +- }, [sessionId, turn, time]); +- const label = useTurnClock((state) => { +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- if (typeof visit?.seen !== "number" || time > visit.seen) return false; +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start > visit.seen && start <= visit.entered; +- }); +- if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null +- ); +function useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -6361,7 +8022,9 @@ diff --git a/client.js b/client.js + for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); + return () => observer.disconnect(); + }, [active, key]); -+} + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +function useNow(active) { + const [now, setNow] = (0, import_react12.useState)(Date.now); + (0, import_react12.useEffect)(() => { @@ -6370,12 +8033,67 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [active]); + return now; -+} + } +-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 since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; +var alwaysChoices = (approval, bot) => bot?.remote ? [] : [ + ["always-bot", "Always allow for this Bot"], + ...approval.roomId ? [["always-room", "Always allow for this group"]] : [] -+]; + ]; +-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; +-function toolActivity(name, arg, findBot, self) { +- switch (name) { +- case "ask_user": +- return null; +- case "request_secret": +- return null; +- case "list_secrets": +- return { label: t("Checking the keys"), state: "searching" }; +- case "ask_user_question": +- return { label: t("Waiting for your answer"), state: "alert" }; +- case "group_relay": +- return { relay: true, state: "orbit" }; +- case "message_bot": { +- const target = findBot(arg("to")); +- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; +- } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +- } +- case "create_bot": +- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; +- } +- case "list_bots": +- return { label: t("Checking the team"), state: "searching" }; +- case "read_group_chat": +- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; +- case "read_own_chat": +- return { label: t("Looking back through the chat"), state: "searching" }; +- case "remember": +- return { label: t("Saving to memory"), state: "working" }; +- case "forget": +- return { label: t("Updating memory"), state: "working" }; +- case "recall": +- return { label: t("Checking memory"), state: "searching" }; +- default: +- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; +- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; +- return { label: phrase ? t(phrase) : null, state: toolState(name) }; +function ApprovalCard({ approval, bot, actions }) { + const [busy, setBusy] = (0, import_react12.useState)(false); + const answer = (choice) => { @@ -6411,7 +8129,7 @@ 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 })) + ); -+ } + } + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); @@ -6427,13 +8145,23 @@ diff --git a/client.js b/client.js + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) + ); -+} + } +-function workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +function useMemberWork(room2, useWork) { + const work = useWork?.((value) => value) ?? NO_WORK; + const entries2 = room2 ? memberWork(room2, work) : []; + const now = useNow(entries2.some((entry) => entry.state !== "idle")); + return { entries: entries2, now }; -+} + } +-function useLiveNote(actions, sessionId, id, entry) { +- const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { +- if (!key) return void 0; +- actions.noteLive(sessionId, id, JSON.parse(key)); +- return () => actions.noteLive(sessionId, id, null); +- }, [sessionId, id, key]); +var MEMBER_CSS = ` +.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} +.bt-mw-row{pointer-events:auto;display:flex;align-items:center;gap:10px;min-width:0;max-width:calc(100% + 6px);height:32px;margin-inline-start:-6px;padding:0 12px 0 6px;border:0;border-radius:16px;background:none;color:var(--bt-ink-2);font:inherit;font-size:14px;cursor:pointer;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards;transition:background-color .16s ease-out} @@ -6467,6 +8195,61 @@ diff --git a/client.js b/client.js +@media (prefers-reduced-motion:reduce){.bt-mw-row{animation:none}} +`; + ++// src/shared/task-board.js ++var COLUMNS = ["not-started", "in-progress", "done"]; ++var PRIORITIES = ["urgent", "high", "normal", "low"]; ++var COLUMN_OF = { ++ open: "not-started", ++ claimed: "not-started", ++ working: "in-progress", ++ blocked: "in-progress", ++ "result-ready": "in-progress", ++ accepted: "done", ++ cancelled: "done", ++ failed: "done" ++}; ++function columnOf(task) { ++ return COLUMN_OF[task?.status] ?? "not-started"; ++} ++function priorityOf(task) { ++ return PRIORITIES.includes(task?.priority) ? task.priority : "normal"; ++} ++var PRIORITY_RANK = { urgent: 0, high: 1, normal: 2, low: 3 }; ++function workBucket(task) { ++ if (task?.status === "open" || task?.status === "claimed" || task?.status === "working") return 0; ++ if (task?.status === "blocked" || task?.status === "result-ready") return 1; ++ return 2; ++} ++function compareForWork(a, b) { ++ const byBucket = workBucket(a) - workBucket(b); ++ if (byBucket !== 0) return byBucket; ++ const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); ++ if (byPriority !== 0) return byPriority; ++ const byWorking = (a?.status === "working" ? 0 : 1) - (b?.status === "working" ? 0 : 1); ++ if (byWorking !== 0) return byWorking; ++ return (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0); ++} ++function sameRole(a, b) { ++ const left = String(a ?? "").trim().toLowerCase(); ++ const right = String(b ?? "").trim().toLowerCase(); ++ return left !== "" && left === right; ++} ++function compareForQueue(a, b) { ++ const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); ++ if (byPriority !== 0) return byPriority; ++ return (a?.createdAt ?? 0) - (b?.createdAt ?? 0); ++} ++function progressOf(task) { ++ const value = task?.progress; ++ if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; ++ return Math.max(0, Math.min(100, Math.round(value))); + } +-function 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; ++ +// src/client/inbox.js +var import_react13 = require("react"); +var KEEP_MS = 7 * 864e5; @@ -6514,7 +8297,20 @@ diff --git a/client.js b/client.js + (0, import_react13.useEffect)(() => { + taskListeners.add(setTasks); + let stopped = false; -+ let timer; + let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +- } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); +- }; +- void tick(); + const load = () => Promise.resolve(actions?.tasks?.({ archive: true })).then((list) => { + if (!stopped && list !== void 0) publishTasks(list); + }).catch(() => { @@ -6522,11 +8318,19 @@ diff --git a/client.js b/client.js + if (!stopped) timer = setTimeout(load, POLL_MS2); + }); + load(); -+ return () => { + return () => { +- stop = true; + stopped = true; -+ clearTimeout(timer); + clearTimeout(timer); + taskListeners.delete(setTasks); -+ }; + }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { +- try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; + }, []); + return tasks; +} @@ -6547,17 +8351,88 @@ diff --git a/client.js b/client.js + }); + load(); + workflowPoll = setInterval(load, POLL_MS2); -+ } + } +- } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; +- const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-group-replies" }, +- replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); +- } +- if (reply.kind === "failed") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, +- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) +- ); +- } +- if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) +- ); +- } +- if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); +- } +- if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); +- } +- if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); +- } +- if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( +- "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 () => { + workflowListeners.delete(setWorkflows); + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; -+ } + } +- 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 })) +- ); +- }) +- ); + }; + }, []); + return workflows; -+} + } +-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 = []; +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -6671,7 +8546,13 @@ diff --git a/client.js b/client.js + const count = room2.pendingCount ?? 0; + if (count > 0) held(room2.id, count, { room: room2, place: placeOf(roster, room2.id), ...people(roster, null) }); + } -+ const seen = /* @__PURE__ */ new Set(); + 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); + const running = (botId, entry, roomId) => { + const bot = ownBot(roster, botId); + const key = `${botId}|${roomId ?? ""}`; @@ -6689,7 +8570,43 @@ diff --git a/client.js b/client.js + chat: roomId ?? bot.id, + ...people(roster, bot.id) + }); -+ }; + }; +- visit(root); +- return calls; +-} +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +-} +-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; + for (const [botId, entry] of Object.entries(work.bots ?? {})) running(botId, entry, entry.roomId); + for (const [roomId, entries2] of Object.entries(work.rooms ?? {})) for (const entry of entries2) running(entry.botId, entry, roomId); + const lastEnded = /* @__PURE__ */ new Map(); @@ -6735,8 +8652,30 @@ diff --git a/client.js b/client.js + if (fresh(task.updatedAt)) needs.push({ ...card, status: "failed", reason: lastWhy(task) }); + } else if (task.status === "result-ready") needs.push({ ...card, status: "result", reason: task.result?.summary ?? "" }); + else if (task.status === "blocked") needs.push({ ...card, status: task.waitingFor === "input" ? "input" : "approval", reason: task.inputRequest?.question ?? "" }); -+ else if (task.status === "open" || task.status === "claimed" || task.status === "working") progress.push({ ...card, status: "task", since: task.createdAt }); -+ else if ((task.status === "accepted" || task.status === "cancelled") && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "accepted" ? "done" : "cancelled" }); ++ else { ++ 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" }); + } +- 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 }); + } + for (const graph of Array.isArray(workflows) ? workflows : []) { + if (graph === null || typeof graph !== "object") continue; @@ -6748,7 +8687,11 @@ 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; -+ } + } +- 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 (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")); @@ -6777,7 +8720,16 @@ diff --git a/client.js b/client.js + const text2 = previewOf(sessions, part, [], [], botNames(roster)); + if (text2 === "") continue; + done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text2, place: "", chat: bot.id, ...people(roster, bot.id) }); -+ } + } +- 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 newest = (a, b) => (b.at ?? Infinity) - (a.at ?? Infinity); + for (const card of [...needs, ...progress, ...done]) { @@ -6787,6 +8739,10 @@ diff --git a/client.js b/client.js + return { needs: needs.sort(newest), progress: progress.sort(newest), done: done.sort(newest).slice(0, DONE_MAX) }; +} +var needsCount = (roster, work, tasks, now, workflows = []) => inboxModel({ roster, work, tasks, now, workflows }).needs.length; ++function startedAtOf(task) { ++ const started = (task.actors ?? []).filter((actor) => actor.status === "working" && typeof actor.at === "number"); ++ return started.length ? started.at(-1).at : task.updatedAt; ++} +function visibleNeeds(model, answered) { + return { ...model, needs: model.needs.filter((card) => !answered?.[card.id]) }; +} @@ -6860,7 +8816,11 @@ diff --git a/client.js b/client.js + if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; + event.preventDefault(); + send(); -+ } + } +- 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 })); + }) : null, + slot.open ? (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Send it back")) : (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + slot.open = true; @@ -6885,7 +8845,11 @@ diff --git a/client.js b/client.js + if (selected.length === 0 && custom === "") return; + onAnswer(card, { selected, custom }); + return; -+ } + } +- 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; + if (custom === "") return; + onAnswer(card, custom); + }; @@ -6894,7 +8858,12 @@ diff --git a/client.js b/client.js + if (!multi) { + onAnswer(card, { selected: [label], custom: "" }); + return; -+ } + } +- case "remember": +- case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; +- default: +- return null; + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -7036,7 +9005,7 @@ diff --git a/client.js b/client.js + if (card?.kind === "workflow") { + actions.openSettings("workflow"); + return; -+ } + } + actions.openSession(card.chat); +} +function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { @@ -7115,7 +9084,22 @@ diff --git a/client.js b/client.js + ) + ) + ); -+} + } +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +- const roster = useRoster((value) => value); +- const root = node.data.root; +- const turn = node.location?.turn?.turn ?? root.turn; +- const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); +- const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); +- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; +- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); +- if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +var 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)} @@ -7166,56 +9150,6 @@ diff --git a/client.js b/client.js +.bt-inbox-actions .bt-send{height:28px;font-size:12px} +`; + -+// src/shared/task-board.js -+var COLUMNS = ["not-started", "in-progress", "done"]; -+var PRIORITIES = ["urgent", "high", "normal", "low"]; -+var COLUMN_OF = { -+ open: "not-started", -+ claimed: "not-started", -+ working: "in-progress", -+ blocked: "in-progress", -+ "result-ready": "in-progress", -+ accepted: "done", -+ cancelled: "done", -+ failed: "done" -+}; -+function columnOf(task) { -+ return COLUMN_OF[task?.status] ?? "not-started"; -+} -+function priorityOf(task) { -+ return PRIORITIES.includes(task?.priority) ? task.priority : "normal"; -+} -+var PRIORITY_RANK = { urgent: 0, high: 1, normal: 2, low: 3 }; -+function workBucket(task) { -+ if (task?.status === "open" || task?.status === "claimed" || task?.status === "working") return 0; -+ if (task?.status === "blocked" || task?.status === "result-ready") return 1; -+ return 2; -+} -+function compareForWork(a, b) { -+ const byBucket = workBucket(a) - workBucket(b); -+ if (byBucket !== 0) return byBucket; -+ const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); -+ if (byPriority !== 0) return byPriority; -+ const byWorking = (a?.status === "working" ? 0 : 1) - (b?.status === "working" ? 0 : 1); -+ if (byWorking !== 0) return byWorking; -+ return (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0); -+} -+function sameRole(a, b) { -+ const left = String(a ?? "").trim().toLowerCase(); -+ const right = String(b ?? "").trim().toLowerCase(); -+ return left !== "" && left === right; -+} -+function compareForQueue(a, b) { -+ const byPriority = (PRIORITY_RANK[priorityOf(a)] ?? 2) - (PRIORITY_RANK[priorityOf(b)] ?? 2); -+ if (byPriority !== 0) return byPriority; -+ return (a?.createdAt ?? 0) - (b?.createdAt ?? 0); -+} -+function progressOf(task) { -+ const value = task?.progress; -+ if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; -+ return Math.max(0, Math.min(100, Math.round(value))); -+} -+ +// src/shared/board-card.js +function taskCardKey(id) { + const text2 = String(id ?? "").trim(); @@ -7230,8 +9164,18 @@ diff --git a/client.js b/client.js + id: String(task.id), + title: String(task.title ?? "") + }; -+} -+ + } + +-// src/client/activity-row.js +-var import_react7 = require("react"); +-var import_react_dom2 = require("react-dom"); +-var PHRASE_HOLD_MS = 800; +-var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +// src/client/board-card.js +var import_react14 = require("react"); +var TASK_SKIN = { @@ -7293,7 +9237,13 @@ diff --git a/client.js b/client.js + footer, + actions + ); -+} + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; + +// src/client/kanban.js +var import_react15 = require("react"); @@ -7372,7 +9322,24 @@ 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); -+ } + } +- 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; + }; + 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 })); @@ -7409,7 +9376,43 @@ diff --git a/client.js b/client.js + default: + return t("Not started"); + } -+} + } +-function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); +- check.current = replacing; +- (0, import_react7.useEffect)(() => { +- const seen = /* @__PURE__ */ new WeakSet(); +- const started = Date.now(); +- let column = null; +- let frame = 0; +- const settle = (event) => { +- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; +- delete event.currentTarget.dataset.btEnter; +- event.currentTarget.removeEventListener("animationend", settle); +- }; +- const scan = () => { +- frame = 0; +- const next = document.querySelector("[data-chat-flow]"); +- if (next !== column) { +- observer.disconnect(); +- column = next; +- if (column) observer.observe(column, { childList: true, subtree: true }); +- } +- if (!column) return; +- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; +- let last = -1; +- items.forEach((item, index) => { +- if (seen.has(item)) last = index; +- }); +- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); +- for (const item of fresh) seen.add(item); +- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; +- for (const item of fresh) { +- if (items.indexOf(item) < last) continue; +- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); +- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; +- item.addEventListener("animationend", settle); +- } +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -7458,13 +9461,20 @@ diff --git a/client.js b/client.js + if (stopped || !Array.isArray(list)) return; + setArchive(list.filter((task) => columnOf(task) === "done")); + }).catch(() => void 0); -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); +- }); +- scan(); + load(); + const timer = setInterval(load, POLL_MS3); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + stopped = true; + clearInterval(timer); -+ }; + }; +- }, []); + }, [key]); + const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + const replace = (task) => { @@ -7475,7 +9485,85 @@ diff --git a/client.js b/client.js + }; + const done = scope?.kind === "group" ? archive : archive.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + return { own, tasks, archive: done, replace }; -+} + } +-function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { +- const roster = useRoster((value) => value); +- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); +- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); +- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); +- const bot = roster.byId[sessionId]; +- const room2 = roster.roomsById[sessionId]; +- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); +- const streaming = entries2.some((entry) => entry.text); +- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); +- const relaying = tools.some((entry) => entry.relay); +- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); +- const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); +- const inputAt = Math.max(inputRef.current, pendingInput); +- inputRef.current = inputAt; +- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; +- const [shown, setShown] = (0, import_react7.useState)(null); +- const wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); +- if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { +- if (wantKey === null || shown?.key === wantKey) return void 0; +- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); +- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; +- if (wait <= 0) { +- make(); +- return void 0; +- } +- const timer = setTimeout(make, wait); +- return () => clearTimeout(timer); +- }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); +- leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { +- if (visible) { +- setPhase("in"); +- return void 0; +- } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); +- return () => clearTimeout(timer); +- }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { +- if (phase !== null) return; +- endedAt.current = Date.now(); +- setShown(null); +- const before = shift.current; +- shift.current = null; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (before === null || !scroller || reducedMotion()) return; +- const delta = before - scroller.scrollTop; +- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); +- }, [phase]); +- (0, import_react7.useEffect)(() => { +- if (!running) actions.clearLive(sessionId); +- }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { +- if (phase !== "in") return void 0; +- const timer = setInterval(() => setNow(Date.now()), 1e4); +- return () => clearInterval(timer); +- }, [phase]); +- const host = useRunningHost(running); +- if (!host || phase === null || !shown) return null; +- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( +- "div", +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( +function RailButton({ row, selected, meta, onSelect }) { + return (0, import_react15.createElement)( + "button", @@ -7522,11 +9610,16 @@ diff --git a/client.js b/client.js + question ? (0, import_react15.createElement)("span", { key: "wait", className: "bt-board-wait" }, question) : null + ], + footer: (0, import_react15.createElement)( -+ "span", + "span", +- { className: "bt-typing-text" }, +- (0, import_react7.createElement)( + { className: "bt-board-foot" }, + who ? (0, import_react15.createElement)("span", { className: "bt-board-who" }, who) : null, + (0, import_react15.createElement)( -+ "span", + "span", +- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null + { className: "bt-board-progress" }, + (0, import_react15.createElement)( + "span", @@ -7534,7 +9627,7 @@ diff --git a/client.js b/client.js + (0, import_react15.createElement)("span", { style: { width: `${progress}%` } }) + ), + (0, import_react15.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress })) -+ ) + ) + ), + actions: [ + movable && column === "not-started" ? (0, import_react15.createElement)("button", { key: "start", type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "in-progress") }, t("Start")) : null, @@ -7767,9 +9860,19 @@ diff --git a/client.js b/client.js + onDrag: setDragging, + onCreate: create + }) -+ ) + ) +- ), host); + ); -+} + } +- +-// src/client/dsh-offer.js +-var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +-var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; +-var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); +-function showsDshRow(dsh, app) { +- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; +- if (dsh.kind === "npm") return true; +- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -7831,9 +9934,9 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} -+} + } +`; -+ + // src/client/sidebar.js -var import_react8 = require("react"); +var import_react16 = require("react"); @@ -7898,7 +10001,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4785,55 +9132,131 @@ +@@ -4785,55 +9140,131 @@ onClick: () => actions.openSession(id), onContextMenu }, @@ -8045,7 +10148,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +9264,20 @@ +@@ -4841,20 +9272,20 @@ actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -8074,7 +10177,7 @@ diff --git a/client.js b/client.js if (!renaming) return; input.current?.focus(); input.current?.select(); -@@ -4875,10 +9298,10 @@ +@@ -4875,10 +9306,10 @@ } })(); }; @@ -8087,7 +10190,7 @@ diff --git a/client.js b/client.js ref: input, defaultValue: session.title, "aria-label": t("Rename"), -@@ -4891,7 +9314,7 @@ +@@ -4891,7 +9322,7 @@ }) ); } @@ -8096,7 +10199,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4905,8 +9328,8 @@ +@@ -4905,8 +9336,8 @@ actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -8107,7 +10210,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +9364,10 @@ +@@ -4941,10 +9372,10 @@ 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; @@ -8120,7 +10223,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +9382,30 @@ +@@ -4959,30 +9390,30 @@ actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -8162,7 +10265,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +9419,8 @@ +@@ -4996,8 +9427,8 @@ } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -8173,7 +10276,7 @@ diff --git a/client.js b/client.js const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +9437,44 @@ +@@ -5014,9 +9445,44 @@ tops.current = { layout, map: next }; }, [layout, key]); } @@ -8219,7 +10322,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); -@@ -5025,17 +9483,26 @@ +@@ -5025,17 +9491,26 @@ for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); @@ -8250,7 +10353,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 +9512,42 @@ +@@ -5045,39 +9520,42 @@ event.preventDefault(); actions.openPalette(event.key); }; @@ -8313,7 +10416,7 @@ diff --git a/client.js b/client.js 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 +9586,14 @@ +@@ -5116,14 +9594,14 @@ setDshError(""); void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); }; @@ -8333,7 +10436,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-round bt-update", -@@ -5133,21 +9603,21 @@ +@@ -5133,21 +9611,21 @@ "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle @@ -8366,7 +10469,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: phase === "installing", -@@ -5155,17 +9625,17 @@ +@@ -5155,17 +9633,17 @@ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, @@ -8391,7 +10494,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: dshPhase === "installing", -@@ -5176,61 +9646,25 @@ +@@ -5176,61 +9654,25 @@ ), document.body) : null ); } @@ -8463,7 +10566,7 @@ diff --git a/client.js b/client.js ); } var openShellSettings = null; -@@ -5259,6 +9693,18 @@ +@@ -5259,6 +9701,18 @@ else openHarnessSettings(); }); } @@ -8482,7 +10585,7 @@ diff --git a/client.js b/client.js function openThroughMenu(trigger) { const before = new Set(document.querySelectorAll('[role="menu"]')); trigger.click(); -@@ -5288,32 +9734,32 @@ +@@ -5288,32 +9742,32 @@ })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { @@ -8523,7 +10626,7 @@ diff --git a/client.js b/client.js 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 +9787,32 @@ +@@ -5341,30 +9795,32 @@ const groups = [ [ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -8568,7 +10671,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -5372,7 +9820,7 @@ +@@ -5372,7 +9828,7 @@ setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -8577,7 +10680,7 @@ diff --git a/client.js b/client.js )) ), document.body) : null ); -@@ -5382,8 +9830,8 @@ +@@ -5382,8 +9838,8 @@ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); @@ -8588,7 +10691,7 @@ diff --git a/client.js b/client.js if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +9845,40 @@ +@@ -5397,40 +9853,40 @@ return void 0; } const last = lastBot.current; @@ -8639,7 +10742,7 @@ diff --git a/client.js b/client.js } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5456,7 +9904,7 @@ +@@ -5456,7 +9912,7 @@ .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)} @@ -8648,7 +10751,7 @@ diff --git a/client.js b/client.js @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 +9939,140 @@ +@@ -5491,18 +9947,140 @@ .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}} @@ -8794,7 +10897,7 @@ diff --git a/client.js b/client.js const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; -@@ -5512,19 +10082,19 @@ +@@ -5512,19 +10090,19 @@ return shown; } function MemoryNotice({ shown }) { @@ -8820,7 +10923,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); const firstTurns = useSessions((list) => { -@@ -5537,7 +10107,7 @@ +@@ -5537,7 +10115,7 @@ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); @@ -8829,7 +10932,7 @@ diff --git a/client.js b/client.js if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); -@@ -5548,52 +10118,41 @@ +@@ -5548,52 +10126,41 @@ const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; const detailsOpen = useUi((state) => state.details === sessionId); @@ -8879,15 +10982,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_react18.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react18.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react18.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_react18.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react18.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react18.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_react18.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + (0, import_react18.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), + (0, import_react18.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), @@ -8902,7 +11004,7 @@ diff --git a/client.js b/client.js let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +10201,7 @@ +@@ -5642,7 +10209,7 @@ }, []); return view; } @@ -8911,7 +11013,7 @@ diff --git a/client.js b/client.js const replies = useSessions((list) => { const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -@@ -5658,18 +10217,22 @@ +@@ -5658,18 +10225,22 @@ }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; @@ -8939,7 +11041,7 @@ diff --git a/client.js b/client.js const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +10240,16 @@ +@@ -5677,16 +10248,16 @@ }, [showDown]); if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); @@ -8960,7 +11062,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), -@@ -5694,7 +10257,7 @@ +@@ -5694,7 +10265,7 @@ event.stopPropagation(); onDismiss(); } @@ -8969,23 +11071,32 @@ diff --git a/client.js b/client.js ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5703,9 +10266,9 @@ +@@ -5702,31 +10273,147 @@ + if (back) back.click(); else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); }; - const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); +- const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); - return (0, import_react_dom4.createPortal)( - (0, import_react9.createElement)( - import_react9.Fragment, +- null, +- showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, +- showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null +- ), +- document.body +- ); ++ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); + return (0, import_react_dom5.createPortal)( + (0, import_react18.createElement)( + import_react18.Fragment, - null, - showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, - showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -@@ -5714,19 +10277,135 @@ - ); - } - ++ null, ++ showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, ++ showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null ++ ), ++ document.body ++ ); ++} ++ +// src/client/composer-label.js +var ATTRIBUTES = ["aria-label", "data-placeholder"]; +function holdComposerLabel(doc, label) { @@ -9100,8 +11211,8 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; -+} -+ + } + // src/client/composer.js -var import_react10 = require("react"); -var import_react_dom5 = require("react-dom"); @@ -9126,7 +11237,7 @@ diff --git a/client.js b/client.js if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); -@@ -5745,10 +10424,10 @@ +@@ -5745,10 +10432,10 @@ let interim = ""; for (let index = event.resultIndex; index < event.results.length; index += 1) { const result = event.results[index]; @@ -9140,7 +11251,7 @@ diff --git a/client.js b/client.js } if (interim) latest.current(interim, false); }; -@@ -5774,19 +10453,38 @@ +@@ -5774,19 +10461,38 @@ const stop = () => recognition.current?.stop(); return { listening, note, start, stop }; } @@ -9184,7 +11295,7 @@ diff --git a/client.js b/client.js const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); if (box instanceof HTMLElement) { box.focus(); -@@ -5797,10 +10495,10 @@ +@@ -5797,10 +10503,10 @@ }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; @@ -9198,7 +11309,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +10506,16 @@ +@@ -5808,16 +10514,16 @@ title: dictation.listening ? t("Stop dictation") : t("Dictate"), onMouseDown: keepComposerFocus, onClick: () => dictation.listening ? dictation.stop() : dictation.start() @@ -9219,7 +11330,7 @@ diff --git a/client.js b/client.js ); } function menuIcons(roster) { -@@ -5825,17 +10523,17 @@ +@@ -5825,17 +10531,17 @@ return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; @@ -9242,7 +11353,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +10574,10 @@ +@@ -5876,10 +10582,10 @@ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); @@ -9256,7 +11367,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-plus", -@@ -5889,8 +10587,8 @@ +@@ -5889,8 +10595,8 @@ "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle @@ -9267,7 +11378,7 @@ diff --git a/client.js b/client.js "div", { ref: menu, -@@ -5900,9 +10598,9 @@ +@@ -5900,9 +10606,9 @@ style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, @@ -9280,7 +11391,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -5922,15 +10620,57 @@ +@@ -5922,15 +10628,57 @@ function mentionRows(roster, sessionId, query = "") { const here = conversationOf(roster, sessionId); if (here === null) return null; @@ -9342,7 +11453,7 @@ diff --git a/client.js b/client.js function chatRows(roster, list, sessionId) { const here = conversationOf(roster, sessionId); if (here === null) return []; -@@ -5959,14 +10699,31 @@ +@@ -5959,14 +10707,31 @@ order: -20, showGroupTitle: false, async candidates(session, { query }) { @@ -9378,7 +11489,7 @@ diff --git a/client.js b/client.js }, onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 }; -@@ -5995,6 +10752,83 @@ +@@ -5995,6 +10760,83 @@ return [bots, chats]; } @@ -9462,7 +11573,7 @@ diff --git a/client.js b/client.js // src/client/speech.js var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); -@@ -6008,8 +10842,8 @@ +@@ -6008,8 +10850,8 @@ function waveBytes(samples, rate = 16e3) { const bytes = new Uint8Array(44 + samples.length * 2); const view = new DataView(bytes.buffer); @@ -9473,7 +11584,7 @@ diff --git a/client.js b/client.js }; ascii(0, "RIFF"); view.setUint32(4, 36 + samples.length * 2, true); -@@ -6030,9 +10864,9 @@ +@@ -6030,9 +10872,9 @@ return bytes; } function base64Of(bytes) { @@ -9486,7 +11597,7 @@ diff --git a/client.js b/client.js } var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); async function openCapture() { -@@ -6121,233 +10955,9 @@ +@@ -6121,233 +10963,9 @@ }; } @@ -9722,17 +11833,28 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS = 120; function groupModels(models) { const groups = /* @__PURE__ */ new Map(); -@@ -6367,17 +10977,10 @@ - }); - } - var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); +@@ -6361,23 +10979,16 @@ + const name = list[0].providerName ?? provider; + names.set(name, (names.get(name) ?? 0) + 1); + } +- return [...groups].map(([provider, list]) => { +- const name = list[0].providerName ?? provider; +- return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; +- }); +-} +-var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); -function modelOf(bot, roster) { - const ref = bot.model ?? bot.appliedModel; - if (!ref) return void 0; - const served = servedOf(roster, ref); - const [provider, ...rest] = ref.split("/"); - return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; --} ++ return [...groups].map(([provider, list]) => { ++ const name = list[0].providerName ?? provider; ++ return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; ++ }); + } ++var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); var effortCache = /* @__PURE__ */ new Map(); function useEfforts(served, ref, actions) { - const [fetched, setFetched] = (0, import_react13.useState)(void 0); @@ -9742,7 +11864,7 @@ diff --git a/client.js b/client.js setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; -@@ -6402,12 +11005,12 @@ +@@ -6402,12 +11013,12 @@ ]; } function EffortRows({ reasoning, current, onPick }) { @@ -9760,7 +11882,7 @@ diff --git a/client.js b/client.js "button", { key: row.effort ?? "default", -@@ -6416,17 +11019,17 @@ +@@ -6416,17 +11027,17 @@ "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, @@ -9784,7 +11906,7 @@ diff --git a/client.js b/client.js "button", { key: model.ref, -@@ -6436,20 +11039,20 @@ +@@ -6436,20 +11047,20 @@ title: model.ref, onClick: () => onPick(model.ref) }, @@ -9813,7 +11935,7 @@ diff --git a/client.js b/client.js if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +11087,7 @@ +@@ -6484,6 +11095,7 @@ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { @@ -9821,7 +11943,7 @@ diff --git a/client.js b/client.js const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); const shown = modelOf(bot, roster); const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +11115,10 @@ +@@ -6511,10 +11123,10 @@ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; const models = roster.models ?? []; @@ -9835,7 +11957,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6527,37 +11131,38 @@ +@@ -6527,37 +11139,38 @@ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, onClick: toggle }, @@ -9888,7 +12010,7 @@ diff --git a/client.js b/client.js const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; -@@ -6569,13 +11174,13 @@ +@@ -6569,13 +11182,13 @@ void actions.refreshModels?.(); }).finally(() => setBusy("")); }; @@ -9907,7 +12029,7 @@ diff --git a/client.js b/client.js key: model.ref, type: "button", className: "bt-model-chip", -@@ -6584,50 +11189,115 @@ +@@ -6584,50 +11197,115 @@ "aria-label": `${model.name} · ${provider}`, title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) @@ -10039,7 +12161,7 @@ diff --git a/client.js b/client.js ...ops, ...hooks, t: actions.accountT(), -@@ -6639,6 +11309,16 @@ +@@ -6639,6 +11317,16 @@ }); } var MODEL_CSS = ` @@ -10056,7 +12178,7 @@ diff --git a/client.js b/client.js .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} -@@ -6675,18 +11355,96 @@ +@@ -6675,18 +11363,96 @@ @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} `; @@ -10161,7 +12283,7 @@ diff --git a/client.js b/client.js const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { -@@ -6719,35 +11477,35 @@ +@@ -6719,35 +11485,35 @@ }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -10213,7 +12335,7 @@ diff --git a/client.js b/client.js className: "bt-folder-input", autoFocus: true, value: draft, -@@ -6759,33 +11517,33 @@ +@@ -6759,33 +11525,33 @@ if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), @@ -10263,7 +12385,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6798,19 +11556,19 @@ +@@ -6798,19 +11564,19 @@ title: value, onClick: toggle }, @@ -10289,7 +12411,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -6830,8 +11588,8 @@ +@@ -6830,8 +11596,8 @@ `; // src/client/schedules.js @@ -10300,7 +12422,7 @@ diff --git a/client.js b/client.js var shared = null; var listeners = /* @__PURE__ */ new Set(); function publish(view) { -@@ -6839,8 +11597,8 @@ +@@ -6839,8 +11605,8 @@ for (const listener of listeners) listener(view); } function useSchedules(actions) { @@ -10311,7 +12433,7 @@ diff --git a/client.js b/client.js listeners.add(setView); let stopped = false; let timer; -@@ -6848,7 +11606,7 @@ +@@ -6848,7 +11614,7 @@ if (!stopped) publish(next); }).catch(() => { }).finally(() => { @@ -10320,7 +12442,7 @@ diff --git a/client.js b/client.js }); load(); return () => { -@@ -6873,7 +11631,8 @@ +@@ -6873,7 +11639,8 @@ function everyText(seconds) { if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); @@ -10330,7 +12452,7 @@ diff --git a/client.js b/client.js } function timingText(task) { const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -@@ -6887,8 +11646,8 @@ +@@ -6887,8 +11654,8 @@ return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; case "weekly": { const days = (task.weekdays ?? []).join(); @@ -10341,7 +12463,7 @@ diff --git a/client.js b/client.js } case "cron": return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -@@ -6896,11 +11655,25 @@ +@@ -6896,11 +11663,25 @@ return ""; } } @@ -10372,7 +12494,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -6913,20 +11686,49 @@ +@@ -6913,20 +11694,49 @@ setBusy(true); void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; @@ -10431,7 +12553,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, -@@ -6934,7 +11736,7 @@ +@@ -6934,7 +11744,7 @@ "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove @@ -10440,7 +12562,7 @@ diff --git a/client.js b/client.js ); } var pad = (number) => String(number).padStart(2, "0"); -@@ -6944,27 +11746,49 @@ +@@ -6944,27 +11754,49 @@ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; } var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; @@ -10507,7 +12629,7 @@ diff --git a/client.js b/client.js setBusy(true); setError(""); let when3; -@@ -6975,151 +11799,298 @@ +@@ -6975,151 +11807,298 @@ setBusy(false); return; } @@ -10868,7 +12990,7 @@ diff --git a/client.js b/client.js .bt-task[data-done]{opacity:.6} .bt-task[data-busy]{opacity:.4} .bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} -@@ -7127,6 +12098,12 @@ +@@ -7127,6 +12106,12 @@ .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} .bt-task-remove svg{width:14px;height:14px} @@ -10881,7 +13003,7 @@ diff --git a/client.js b/client.js .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} .bt-task-head{display:flex;align-items:center;gap:6px} .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -@@ -7158,12 +12135,12 @@ +@@ -7158,12 +12143,12 @@ `; // src/client/capability-settings.js @@ -10897,7 +13019,7 @@ diff --git a/client.js b/client.js } catch { throw new Error(t("{label} must be valid JSON.", { label })); } -@@ -7172,11 +12149,11 @@ +@@ -7172,11 +12157,11 @@ }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { @@ -10912,7 +13034,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, -@@ -7188,7 +12165,7 @@ +@@ -7188,7 +12173,7 @@ ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { @@ -10921,7 +13043,7 @@ diff --git a/client.js b/client.js const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); -@@ -7199,7 +12176,7 @@ +@@ -7199,7 +12184,7 @@ if (draft.kind === "mcp" && draft.transport === "stdio") { payload.transport = "stdio"; payload.command = draft.command; @@ -10930,7 +13052,7 @@ diff --git a/client.js b/client.js payload.env = parseMap(draft.envText, t("Environment")); } else if (draft.kind === "mcp") { payload.transport = "streamable-http"; -@@ -7219,95 +12196,95 @@ +@@ -7219,95 +12204,95 @@ } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); @@ -11066,7 +13188,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); -@@ -7330,20 +12307,20 @@ +@@ -7330,20 +12315,20 @@ setConfirming(""); void run(actions.deleteCapability(item.name)); }; @@ -11094,7 +13216,7 @@ diff --git a/client.js b/client.js key: item.name, item, confirming: confirming === item.name, -@@ -7359,10 +12336,10 @@ +@@ -7359,10 +12344,10 @@ const attached = new Set(bot.capabilities ?? []); const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; @@ -11107,7 +13229,7 @@ diff --git a/client.js b/client.js key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7372,9 +12349,55 @@ +@@ -7372,9 +12357,55 @@ ); } @@ -11165,7 +13287,7 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,13 +12407,13 @@ +@@ -7384,13 +12415,13 @@ for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { @@ -11182,7 +13304,7 @@ diff --git a/client.js b/client.js if (!stop) noteCount(botId, view.entries.length); }).catch(() => { }); -@@ -7404,16 +12427,16 @@ +@@ -7404,16 +12435,16 @@ function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -11205,7 +13327,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7428,45 +12451,45 @@ +@@ -7428,45 +12459,45 @@ return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { @@ -11270,7 +13392,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7478,9 +12501,9 @@ +@@ -7478,9 +12509,9 @@ return groups; }, []); function EntryRow({ entry, remove }) { @@ -11283,7 +13405,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -7495,16 +12518,16 @@ +@@ -7495,16 +12526,16 @@ setBusy(true); if (!await remove(entry)) setBusy(false); }; @@ -11305,7 +13427,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, -@@ -7512,7 +12535,7 @@ +@@ -7512,7 +12543,7 @@ "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click @@ -11314,7 +13436,7 @@ diff --git a/client.js b/client.js ); } function Entries({ bot, view, remove }) { -@@ -7521,82 +12544,82 @@ +@@ -7521,82 +12552,82 @@ { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; @@ -11432,7 +13554,7 @@ diff --git a/client.js b/client.js alive.current = true; void load({ summarize: true }); return () => { -@@ -7606,9 +12629,9 @@ +@@ -7606,9 +12637,9 @@ }, [botId]); return { view, error, load, show }; } @@ -11445,7 +13567,7 @@ diff --git a/client.js b/client.js const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); -@@ -7616,18 +12639,18 @@ +@@ -7616,18 +12647,18 @@ } function MemoryDialog({ bot, actions, leaving = false }) { const { view, error, load, show } = useMemoryView(bot.id, actions); @@ -11474,7 +13596,7 @@ diff --git a/client.js b/client.js if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7671,7 +12694,7 @@ +@@ -7671,7 +12702,7 @@ }; const ask = async (event) => { event.preventDefault(); @@ -11483,7 +13605,7 @@ diff --git a/client.js b/client.js if (asked === "" || answer?.pending) return; setText(""); setAnswer({ asked, pending: true }); -@@ -7689,65 +12712,65 @@ +@@ -7689,65 +12720,65 @@ const when3 = view?.summary ? ago(view.summary.at, now) : null; const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; const busy = answer?.pending === true; @@ -11576,7 +13698,7 @@ diff --git a/client.js b/client.js ) ) ) -@@ -7755,8 +12778,40 @@ +@@ -7755,8 +12786,40 @@ ); } @@ -11618,7 +13740,7 @@ diff --git a/client.js b/client.js var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +12835,8 @@ +@@ -7780,8 +12843,8 @@ throw new Error(t("That image is too large")); } function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { @@ -11629,7 +13751,7 @@ diff --git a/client.js b/client.js const commit = () => { const next = draft?.trim(); setDraft(null); -@@ -7792,7 +12847,7 @@ +@@ -7792,7 +12855,7 @@ if (next === void 0 || next === (value ?? "") || required && next === "") return; void onSave(next); }; @@ -11638,7 +13760,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, -@@ -7813,29 +12868,29 @@ +@@ -7813,29 +12876,29 @@ }); } function Row({ label, hint, stack = false, children }) { @@ -11677,7 +13799,7 @@ diff --git a/client.js b/client.js ); } function modelHint(bot, roster) { -@@ -7846,22 +12901,22 @@ +@@ -7846,22 +12909,22 @@ return t("Applies from the next message."); } function ShapeRing({ shape, size }) { @@ -11705,7 +13827,7 @@ diff --git a/client.js b/client.js key: id, type: "button", className: "bt-shape", -@@ -7869,8 +12924,8 @@ +@@ -7869,8 +12932,8 @@ title: t(shapeOf(id).label), "aria-pressed": !hasPhoto && look.shape === id, onClick: () => onShape(id) @@ -11716,7 +13838,7 @@ diff --git a/client.js b/client.js key: color, type: "button", className: "bt-color", -@@ -7878,19 +12933,19 @@ +@@ -7878,19 +12941,19 @@ title: t(capitalize(color)), "aria-pressed": look.color === color, onClick: () => onColor(color) @@ -11742,7 +13864,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); -@@ -7908,7 +12963,7 @@ +@@ -7908,7 +12971,7 @@ window.removeEventListener("keydown", onKey, true); }; }, [onClose]); @@ -11751,7 +13873,7 @@ diff --git a/client.js b/client.js if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +12977,32 @@ +@@ -7922,32 +12985,32 @@ }, [tab, bot]); const hasPhoto = Boolean(look.image); const glide = useGlide(tab); @@ -11792,7 +13914,7 @@ diff --git a/client.js b/client.js "div", { className: "bt-dropzone", -@@ -7963,15 +13018,15 @@ +@@ -7963,15 +13026,15 @@ upload(event.dataTransfer.files?.[0]); } }, @@ -11814,7 +13936,7 @@ diff --git a/client.js b/client.js const file = event.target.files?.[0]; event.target.value = ""; upload(file); -@@ -7982,26 +13037,27 @@ +@@ -7982,26 +13045,27 @@ function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -11850,7 +13972,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 +13067,116 @@ +@@ -8011,149 +13075,812 @@ if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -11860,93 +13982,65 @@ diff --git a/client.js b/client.js }, [bot.id]); - return (0, import_react18.createElement)( + return (0, import_react28.createElement)( - "div", - { className: "bt-bs" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-bs" }, + (0, import_react28.createElement)( - "div", - { className: "bt-bs-hero" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-bs-hero" }, + (0, import_react28.createElement)( - "div", - { className: "bt-avatar-anchor" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-avatar-anchor" }, + (0, import_react28.createElement)( - "button", - { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), -- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) ++ "button", ++ { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, + (0, import_react28.createElement)(BotMark, { bot, size: 64, live: true }), + (0, import_react28.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react28.createElement)(PencilIcon)) - ), -- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ++ ), + editing ? (0, import_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react28.createElement)( - "div", - { className: "bt-bs-who" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) ++ "div", ++ { className: "bt-bs-who" }, + (0, import_react28.createElement)("span", { className: "bt-bs-name" }, bot.name), + (0, import_react28.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) - ) - ), -- (0, import_react18.createElement)( ++ ) ++ ), + (0, import_react28.createElement)( - Card, - { title: t("Profile") }, -- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) ++ Card, ++ { title: t("Profile") }, + (0, import_react28.createElement)(Row, { label: t("Name") }, (0, import_react28.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), + (0, import_react28.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react28.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react28.createElement)( - Card, - { title: t("Model") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Model") }, + (0, import_react28.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) -- ) ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), + (0, import_react28.createElement)(ModelMenu, { bot, roster, actions, run }) + ), + bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react28.createElement)( - Card, - { title: t("Working folder") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Working folder") }, + (0, import_react28.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), + (0, import_react28.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) - ) - ), -- (0, import_react18.createElement)( ++ ) ++ ), + (0, import_react28.createElement)( - Card, - { title: t("Instructions") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Instructions") }, + (0, import_react28.createElement)( - Row, - { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) ++ Row, ++ { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, + (0, import_react28.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) - ) - ), -- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), -- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), -- (0, import_react18.createElement)( ++ ) ++ ), + // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). + (0, import_react28.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react28.createElement)(Card, { title: t("Sharing") }, children) }), + (0, import_react28.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), @@ -11955,73 +14049,59 @@ diff --git a/client.js b/client.js + (0, import_react28.createElement)(BotSchedules, { bot, roster, actions }), + (0, import_react28.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), + (0, import_react28.createElement)( - Card, - { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( ++ Card, ++ { title: t("Main Bot") }, + onlyMain ? (0, import_react28.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react28.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-set-copy" }, + (0, import_react28.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_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) + ) : (0, import_react28.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_react28.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_react28.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_react28.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_react28.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( ++ Card, + { title: t("More") }, + (0, import_react28.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_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), + (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) - ), -- isMainBot ? null : (0, import_react18.createElement)( ++ ), + isMainBot ? null : (0, import_react28.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_react28.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_react28.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), + (0, import_react28.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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) - ) - ) - ); -@@ -8130,9 +13190,17 @@ - .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 @@ -12029,16 +14109,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 +13221,659 @@ - .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. */ @@ -12047,8 +14140,8 @@ diff --git a/client.js b/client.js + .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} + .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} +} - `; - ++`; ++ +// src/client/group-settings.js +var import_react29 = require("react"); +var ROOM_MODES = [ @@ -12219,22 +14312,40 @@ diff --git a/client.js b/client.js + }); + }; + return (0, import_react29.createElement)( -+ "div", -+ { className: "bt-bs" }, + "div", + { className: "bt-bs" }, +- (0, import_react18.createElement)( + (0, import_react29.createElement)( -+ "div", -+ { className: "bt-bs-hero" }, + "div", + { className: "bt-bs-hero" }, +- (0, import_react18.createElement)( +- "div", +- { className: "bt-avatar-anchor" }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, +- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), +- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) +- ), +- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null +- ), +- (0, import_react18.createElement)( + (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), + (0, import_react29.createElement)( -+ "div", -+ { className: "bt-bs-who" }, + "div", + { className: "bt-bs-who" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), +- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) + (0, import_react29.createElement)("span", { className: "bt-bs-name" }, room2.name), + (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -+ ) -+ ), + ) + ), +- (0, import_react18.createElement)( + (0, import_react29.createElement)( -+ Card, -+ { title: t("Profile") }, + Card, + { title: t("Profile") }, +- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), +- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) + (0, import_react29.createElement)( + Row, + { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, @@ -12245,12 +14356,18 @@ diff --git a/client.js b/client.js + { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, + (0, import_react29.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_react18.createElement)( + (0, import_react29.createElement)( -+ Card, + Card, +- { title: t("Model") }, +- (0, import_react18.createElement)( + { title: t("Who answers") }, + (0, import_react29.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), +- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) + { className: "bt-set-row bt-bs-row bt-bs-stack" }, + (0, import_react29.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react29.createElement)("button", { + key: id, @@ -12263,31 +14380,54 @@ diff --git a/client.js b/client.js + } + }, t(label)))), + (0, import_react29.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -+ ) -+ ), + ) + ), +- (0, import_react18.createElement)( + (0, import_react29.createElement)( -+ Card, -+ { title: t("Working folder") }, + Card, + { title: t("Working folder") }, +- (0, import_react18.createElement)( + (0, import_react29.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), + (0, import_react29.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -+ ) -+ ), + ) + ), +- (0, import_react18.createElement)( + face ? (0, import_react29.createElement)( -+ Card, + Card, +- { title: t("Instructions") }, +- (0, import_react18.createElement)( +- Row, +- { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, +- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) +- ) +- ), +- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), +- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), +- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), +- (0, import_react18.createElement)( + { title: t("Members · {count}", { count: people2.length }) }, + people2.map((person) => (0, import_react29.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) + ) : null, + face ? null : (0, import_react29.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_react29.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_react29.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_react29.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), + (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Human")), @@ -12327,7 +14467,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_react29.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_react29.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 }] }) }, "×") + )), @@ -12372,19 +14514,29 @@ 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_react29.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_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), + (0, import_react29.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_react29.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_react29.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_react29.createElement)( -+ Card, + Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( + { title: t("More") }, + (0, import_react29.createElement)( -+ "button", + "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)) + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + (0, import_react29.createElement)( + "span", @@ -12644,7 +14796,17 @@ diff --git a/client.js b/client.js + setQuery(""); + setCursor(0); + } }, (0, import_react30.createElement)(CloseIcon)) : null -+ ), + ), +- isMainBot ? null : (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_react18.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) +- ), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + (0, import_react30.createElement)( + "div", + { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, @@ -12687,15 +14849,47 @@ 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 +13884,48 @@ +@@ -8165,36 +13892,48 @@ } function startModel(roster) { const models = roster.models ?? []; @@ -12759,7 +14953,7 @@ diff --git a/client.js b/client.js if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8206,8 +13937,8 @@ +@@ -8206,8 +13945,8 @@ setError(""); setBusy(true); try { @@ -12770,7 +14964,7 @@ diff --git a/client.js b/client.js } catch (failure) { setError(failure?.message ?? String(failure)); setBusy(false); -@@ -8219,25 +13950,25 @@ +@@ -8219,25 +13958,25 @@ avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; @@ -12804,7 +14998,7 @@ diff --git a/client.js b/client.js look, hasPhoto: Boolean(image), onShape: (shape) => { -@@ -8249,27 +13980,27 @@ +@@ -8249,27 +13988,27 @@ setLook((current) => ({ ...current, color })); } }), @@ -12840,7 +15034,7 @@ diff --git a/client.js b/client.js className: "bt-input", autoFocus: !inline, value: name, -@@ -8284,75 +14015,113 @@ +@@ -8284,75 +14023,113 @@ } } }), @@ -12984,7 +15178,7 @@ diff --git a/client.js b/client.js const pick2 = (bot) => { setPicked((current) => [...current, bot.id]); setQuery(""); -@@ -8386,71 +14155,104 @@ +@@ -8386,71 +14163,104 @@ }; const created = (bot) => { close(); @@ -13109,7 +15303,7 @@ diff --git a/client.js b/client.js canGoBack: draft.fromList, onBack: draft.fromList ? () => { setDraft(null); -@@ -8467,33 +14269,41 @@ +@@ -8467,33 +14277,41 @@ if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; @@ -13166,7 +15360,7 @@ diff --git a/client.js b/client.js ref: inputRef, autoFocus: true, value: query, -@@ -8517,20 +14327,20 @@ +@@ -8517,20 +14335,20 @@ } else if (event.key === "Backspace" && query === "") unpick(); } }), @@ -13192,7 +15386,7 @@ diff --git a/client.js b/client.js "button", { key: option.key, -@@ -8542,21 +14352,36 @@ +@@ -8542,21 +14360,36 @@ onMouseEnter: () => setCursor(index), onClick: option.run }, @@ -13238,7 +15432,7 @@ diff --git a/client.js b/client.js rows: 1, value: message, placeholder, -@@ -8569,7 +14394,7 @@ +@@ -8569,7 +14402,7 @@ } } }), @@ -13247,7 +15441,7 @@ diff --git a/client.js b/client.js ) ) ) -@@ -8577,18 +14402,41 @@ +@@ -8577,18 +14410,41 @@ } // src/client/exchange.js @@ -13293,7 +15487,7 @@ diff --git a/client.js b/client.js let stop = false; let timer; let seen = null; -@@ -8612,49 +14460,42 @@ +@@ -8612,49 +14468,42 @@ const right = roster.byId[pair[1]]; let lastDay = 0; const paneLeft = usePaneLeft(); @@ -13357,7 +15551,7 @@ diff --git a/client.js b/client.js var VOICE_LABELS = { listening: "Listening", thinking: "Thinking", -@@ -8665,14 +14506,14 @@ +@@ -8665,14 +14514,14 @@ }; var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; function useMicLevel(enabled) { @@ -13375,7 +15569,7 @@ diff --git a/client.js b/client.js if (stopped) { media.getTracks().forEach((track) => track.stop()); return; -@@ -8716,18 +14557,18 @@ +@@ -8716,18 +14565,18 @@ const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); @@ -13405,7 +15599,7 @@ diff --git a/client.js b/client.js const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; -@@ -8735,24 +14576,24 @@ +@@ -8735,24 +14584,24 @@ actions.closeVoice(); }; useEscape(close); @@ -13438,7 +15632,7 @@ diff --git a/client.js b/client.js if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +14608,16 @@ +@@ -8767,16 +14616,16 @@ }; }, [speech]); const listen = phase === "listening"; @@ -13458,7 +15652,7 @@ diff --git a/client.js b/client.js if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; -@@ -8789,9 +14630,9 @@ +@@ -8789,9 +14638,9 @@ setPhase("transcribing"); try { const wave = await current.finish(catalog.maxDurationSeconds); @@ -13470,7 +15664,7 @@ diff --git a/client.js b/client.js else setPhase("listening"); } catch (error) { if (turn.current !== own) return; -@@ -8838,12 +14679,12 @@ +@@ -8838,12 +14687,12 @@ setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); @@ -13487,7 +15681,7 @@ diff --git a/client.js b/client.js setSaid(spoken); setHeard(""); if (!window.speechSynthesis || !spoken) { -@@ -8858,7 +14699,7 @@ +@@ -8858,7 +14707,7 @@ window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); @@ -13496,7 +15690,7 @@ diff --git a/client.js b/client.js const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,291 +14716,605 @@ +@@ -8875,291 +14724,605 @@ const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; @@ -13734,6 +15928,48 @@ diff --git a/client.js b/client.js - 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)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) +- ) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Members · {count}", { count: members.length }) }, +- members.map((member) => { +- const leads = room2.admin === member.id; +- return (0, import_react23.createElement)( + { className: "bt-wf-palette" }, + (0, import_react35.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), + locals.map((bot) => (0, import_react35.createElement)("button", { @@ -13796,48 +16032,7 @@ diff --git a/client.js b/client.js + if (!from || !to) return null; + return (0, import_react35.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" }); + }) - ), -- (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)( -- "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)( ++ ), + (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => { + const members = layer.map((id) => nodes.find((node) => node.id === id)).filter(Boolean); + if (members.length < 2) return null; @@ -13931,54 +16126,10 @@ diff --git a/client.js b/client.js - { 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)) -- ) + }) - ) - ); - } --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_react35.createElement)( + "button", @@ -13994,7 +16145,20 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react35.createElement)("span", null, item.name), + (0, import_react35.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") -+ ), + ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) +- ), +- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) + source === "own" ? (0, import_react35.createElement)( + "span", + { className: "bt-set-actions" }, @@ -14033,9 +16197,7 @@ 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_react35.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( ++ "div", + { className: "bt-wf-page" }, + (0, import_react35.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_react35.createElement)( @@ -14074,18 +16236,14 @@ diff --git a/client.js b/client.js + { className: "bt-set-card" }, + yours.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react35.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) + ) -+ ) + ) +- ) + ) : null, + graph ? (0, import_react35.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-wf-editor" }, + (0, import_react35.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-wf-actions" }, + (0, import_react35.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), + showUse ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, @@ -14100,17 +16258,51 @@ diff --git a/client.js b/client.js + editing: opened?.source === "own" && editing === true + }) + ) : null -+ ); -+} + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function RenameModal({ workflow, onClose, onConfirm }) { + const [name, setName] = (0, import_react35.useState)(workflow?.name ?? ""); + return (0, import_react35.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react35.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)( + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, + (0, import_react35.createElement)("h3", null, t("Rename workflow")), + (0, import_react35.createElement)( @@ -14120,17 +16312,15 @@ diff --git a/client.js b/client.js + (0, import_react35.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react35.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + { className: "bt-modal-actions" }, + (0, import_react35.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react35.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) - ), - 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)( + ) + ); +} @@ -14141,7 +16331,10 @@ diff --git a/client.js b/client.js + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react35.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-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, + (0, import_react35.createElement)("h3", null, t("Delete workflow")), + (0, import_react35.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), @@ -14340,7 +16533,7 @@ diff --git a/client.js b/client.js // src/client/usage-data.js var HOUR = 0; -@@ -9277,13 +15432,13 @@ +@@ -9277,13 +15440,13 @@ } return result; } @@ -14358,7 +16551,7 @@ diff --git a/client.js b/client.js } return String(Math.round(count)); } -@@ -9294,11 +15449,11 @@ +@@ -9294,11 +15457,11 @@ } // src/client/usage-page.js @@ -14372,7 +16565,7 @@ diff --git a/client.js b/client.js var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -@@ -9341,9 +15496,9 @@ +@@ -9341,9 +15504,9 @@ return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { @@ -14385,7 +16578,7 @@ diff --git a/client.js b/client.js if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); -@@ -9363,9 +15518,9 @@ +@@ -9363,9 +15526,9 @@ }, [value]); return shown; } @@ -14397,7 +16590,7 @@ diff --git a/client.js b/client.js className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +15537,7 @@ +@@ -9382,7 +15545,7 @@ sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], @@ -14406,7 +16599,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "room") { -@@ -9392,7 +15547,7 @@ +@@ -9392,7 +15555,7 @@ name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", @@ -14415,7 +16608,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "agent") { -@@ -9411,33 +15566,33 @@ +@@ -9411,33 +15574,33 @@ return labels[kind] ?? kind; } function Tabs({ label, value, options, onChange, boxRef }) { @@ -14460,7 +16653,7 @@ diff --git a/client.js b/client.js "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +15610,83 @@ +@@ -9455,83 +15618,83 @@ return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { @@ -14572,7 +16765,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +15695,69 @@ +@@ -9540,69 +15703,69 @@ title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -14670,7 +16863,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; -@@ -9628,17 +15783,17 @@ +@@ -9628,17 +15791,17 @@ const [from, to] = [anchor, key].sort(); onPick(from, to); }; @@ -14691,7 +16884,7 @@ diff --git a/client.js b/client.js key, type: "button", className: "bt-us-day", -@@ -9654,15 +15809,15 @@ +@@ -9654,15 +15817,15 @@ "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) @@ -14713,7 +16906,7 @@ diff --git a/client.js b/client.js ); }; const shortcuts = [ -@@ -9671,30 +15826,30 @@ +@@ -9671,30 +15834,30 @@ ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; @@ -14752,7 +16945,7 @@ diff --git a/client.js b/client.js const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); -@@ -9710,13 +15865,13 @@ +@@ -9710,13 +15873,13 @@ for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { @@ -14768,7 +16961,7 @@ diff --git a/client.js b/client.js key: ms, className: "bt-us-cell", style: { "--w": week }, -@@ -9724,7 +15879,7 @@ +@@ -9724,7 +15887,7 @@ "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) @@ -14777,7 +16970,7 @@ diff --git a/client.js b/client.js } } const tipSide = hover ? { -@@ -9733,61 +15888,61 @@ +@@ -9733,61 +15896,61 @@ } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); @@ -14865,7 +17058,7 @@ diff --git a/client.js b/client.js let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +15952,7 @@ +@@ -9797,7 +15960,7 @@ const value = await actions.usage(); await resized; if (stopped) return; @@ -14874,7 +17067,7 @@ diff --git a/client.js b/client.js setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { -@@ -9814,7 +15969,7 @@ +@@ -9814,7 +15977,7 @@ }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); @@ -14883,7 +17076,7 @@ diff --git a/client.js b/client.js if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +15978,7 @@ +@@ -9823,7 +15986,7 @@ const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; @@ -14892,7 +17085,7 @@ diff --git a/client.js b/client.js }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +16026,23 @@ +@@ -9871,23 +16034,23 @@ }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); @@ -14924,7 +17117,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -9912,7 +16067,7 @@ +@@ -9912,7 +16075,7 @@ ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; @@ -14933,7 +17126,7 @@ diff --git a/client.js b/client.js key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), -@@ -9924,7 +16079,7 @@ +@@ -9924,7 +16087,7 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); @@ -14942,7 +17135,7 @@ diff --git a/client.js b/client.js key: `m${at}`, kind: "model", glyph: models[at].glyph(32), -@@ -9936,30 +16091,30 @@ +@@ -9936,30 +16099,30 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); @@ -14983,7 +17176,7 @@ diff --git a/client.js b/client.js ) ]; } else { -@@ -9968,39 +16123,39 @@ +@@ -9968,39 +16131,39 @@ const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ @@ -15037,7 +17230,7 @@ diff --git a/client.js b/client.js body ); } -@@ -10150,33 +16305,216 @@ +@@ -10150,33 +16313,216 @@ @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} @keyframes bt-us-fade{from{opacity:0}} @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} @@ -15265,7 +17458,7 @@ diff --git a/client.js b/client.js const run = (task, after) => { setBusy(true); setError(null); -@@ -10184,83 +16522,195 @@ +@@ -10184,83 +16530,195 @@ after?.(); }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; @@ -15376,23 +17569,21 @@ diff --git a/client.js b/client.js + on && bots ? (0, import_react37.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, + on && channel && connector.mode === "app" ? (0, import_react37.createElement)(AllowedSenders, { connector, actions, run, busy }) : null, + on && canPost ? (0, import_react37.createElement)( - "div", - { className: "bt-connector-form" }, -- (0, import_react25.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react25.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: t("GitHub token"), autoFocus: true })), -- (0, import_react25.createElement)( ++ "div", ++ { className: "bt-connector-form" }, + (0, import_react37.createElement)("label", { className: "bt-q-field" }, (0, import_react37.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })), + (0, import_react37.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ "div", ++ { className: "bt-connector-form-row" }, + (0, import_react37.createElement)("span", { className: "bt-connector-link" }), + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send")) + ) + ) : null, + !on && form ? (0, import_react37.createElement)( -+ "div", -+ { className: "bt-connector-form" }, + "div", + { className: "bt-connector-form" }, +- (0, import_react25.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react25.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: t("GitHub token"), autoFocus: true })), +- (0, import_react25.createElement)( + channel ? (0, import_react37.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react37.createElement)("button", { + key: entry.id, + type: "button", @@ -15414,8 +17605,10 @@ diff --git a/client.js b/client.js + })) : (0, import_react37.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react37.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react37.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react37.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, + "div", + { className: "bt-connector-form-row" }, +- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + (0, import_react37.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, @@ -15425,13 +17618,11 @@ diff --git a/client.js b/client.js setForm(false); setToken(""); + setValues({}); - setError(null); - } }, t("Cancel")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) ++ setError(null); ++ } }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) - ) - ) : null, -- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ++ ) ++ ) : null, + on && gateway && picking ? (0, import_react37.createElement)( + "div", + { className: "bt-connector-form" }, @@ -15443,11 +17634,13 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("span", { className: "bt-connector-link" }), + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + setPicking(false); -+ setError(null); -+ } }, t("Cancel")), + setError(null); + } }, t("Cancel")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) -+ ) -+ ) : null, + ) + ) : null, +- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null + error ? (0, import_react37.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ); } @@ -15491,7 +17684,7 @@ diff --git a/client.js b/client.js const date = new Date(time); const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -@@ -10270,18 +16720,18 @@ +@@ -10270,18 +16728,18 @@ if (entry.source === "turn") return failureLabel(entry.code, entry.text); return entry.text; } @@ -15517,7 +17710,7 @@ diff --git a/client.js b/client.js void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +16739,7 @@ +@@ -10289,7 +16747,7 @@ setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); @@ -15526,7 +17719,7 @@ diff --git a/client.js b/client.js const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; -@@ -10307,8 +16757,8 @@ +@@ -10307,8 +16765,8 @@ if (!inline && !copied) setPreview(true); })); }; @@ -15537,7 +17730,7 @@ diff --git a/client.js b/client.js setNote(t("Downloaded the log. Keys, tokens, and your home folder are hidden in it.")); })); const clearLog = () => run(actions.diagnosticsClear().then(clearClientErrors).then(load).then(() => setNote(t("Cleared the error log.")))); -@@ -10317,59 +16767,59 @@ +@@ -10317,59 +16775,59 @@ const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ @@ -15625,7 +17818,7 @@ diff --git a/client.js b/client.js ) ) ]; -@@ -10377,8 +16827,8 @@ +@@ -10377,8 +16835,8 @@ var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); @@ -15636,7 +17829,7 @@ diff --git a/client.js b/client.js if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +16839,16 @@ +@@ -10389,15 +16847,16 @@ }; }, [toast?.token]); if (!toast) return null; @@ -15658,7 +17851,7 @@ diff --git a/client.js b/client.js ); } var FEEDBACK_CSS = ` -@@ -10416,47 +16867,56 @@ +@@ -10416,47 +16875,56 @@ .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} .bt-event-error{color:#e02135} @@ -15729,7 +17922,7 @@ diff --git a/client.js b/client.js let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); -@@ -10466,87 +16926,72 @@ +@@ -10466,87 +16934,72 @@ live = false; }; }, []); @@ -15840,7 +18033,7 @@ diff --git a/client.js b/client.js .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .bt-about-name{font-size:16px;line-height:22px;font-weight:600} .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -@@ -10569,54 +17014,59 @@ +@@ -10569,54 +17022,59 @@ `; // src/client/settings.js @@ -15918,7 +18111,7 @@ diff --git a/client.js b/client.js key: ink, type: "button", className: "bt-swatch", -@@ -10626,75 +17076,86 @@ +@@ -10626,75 +17084,86 @@ style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), @@ -16028,7 +18221,7 @@ diff --git a/client.js b/client.js key: id, type: "button", "aria-pressed": motion === id, -@@ -10706,69 +17167,69 @@ +@@ -10706,69 +17175,69 @@ ); } function BotsPage({ roster, actions, run }) { @@ -16128,7 +18321,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +17240,113 @@ +@@ -10779,104 +17248,113 @@ }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -16283,7 +18476,7 @@ diff --git a/client.js b/client.js const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; -@@ -10884,13 +17354,22 @@ +@@ -10884,13 +17362,22 @@ const isMainBot = isMainOf(roster, menu.id); const isTile = roster.mainBotId === menu.id; const mainCount = (roster.mainBotIds ?? []).length; @@ -16308,7 +18501,7 @@ diff --git a/client.js b/client.js bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -@@ -10901,20 +17380,22 @@ +@@ -10901,20 +17388,22 @@ bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null ], @@ -16337,7 +18530,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10923,14 +17404,14 @@ +@@ -10923,14 +17412,14 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -16356,7 +18549,7 @@ diff --git a/client.js b/client.js const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +17433,14 @@ +@@ -10952,14 +17441,14 @@ [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; @@ -16376,7 +18569,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10968,16 +17449,31 @@ +@@ -10968,16 +17457,31 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } @@ -16409,7 +18602,7 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11038,244 +17534,8 @@ +@@ -11038,244 +17542,8 @@ } } @@ -16655,7 +18848,7 @@ diff --git a/client.js b/client.js var TAB_LABELS = { details: "Details", browser: "Browser" }; var hostOf = (url) => { try { -@@ -11289,10 +17549,10 @@ +@@ -11289,10 +17557,10 @@ var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { @@ -16668,7 +18861,7 @@ diff --git a/client.js b/client.js if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; -@@ -11302,14 +17562,14 @@ +@@ -11302,14 +17570,14 @@ setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; @@ -16687,7 +18880,7 @@ diff --git a/client.js b/client.js key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +17583,42 @@ +@@ -11323,42 +17591,42 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -16745,7 +18938,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), -@@ -11367,87 +17627,195 @@ +@@ -11367,87 +17635,195 @@ void actions.browserCloseTab(bot.id, tabId); } }, "×"), @@ -16777,11 +18970,8 @@ diff --git a/client.js b/client.js useMorph(body, open); - return (0, import_react31.createElement)( + return (0, import_react42.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_react42.createElement)("div", { className: "bt-section-title" }, t(title)), + (0, import_react42.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), + long ? (0, import_react42.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null @@ -16834,8 +19024,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_react42.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_react42.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), + (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), + (0, import_react42.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react42.createElement)(FacePersonRow, { @@ -16986,7 +19179,7 @@ diff --git a/client.js b/client.js if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; -@@ -11457,7 +17825,7 @@ +@@ -11457,7 +17833,7 @@ const bot = owner; const running = useSessionStatus((map) => map.get(part)?.running === true); const doing = useActivity((map) => map[bot?.id ?? sessionId]); @@ -16995,7 +19188,7 @@ diff --git a/client.js b/client.js const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -11467,41 +17835,67 @@ +@@ -11467,41 +17843,67 @@ return false; }); const share = () => { @@ -17079,7 +19272,7 @@ diff --git a/client.js b/client.js ]; } const current = bot?.name ?? room2.name; -@@ -11515,12 +17909,12 @@ +@@ -11515,12 +17917,12 @@ const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ @@ -17096,7 +19289,7 @@ diff --git a/client.js b/client.js className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,98 +17929,97 @@ +@@ -11535,98 +17937,97 @@ setEditing(false); } } @@ -17240,7 +19433,7 @@ diff --git a/client.js b/client.js var room = { rule: null, selector: "" }; var roomRule = () => { const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -@@ -11674,11 +18067,11 @@ +@@ -11674,11 +18075,11 @@ var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); @@ -17257,7 +19450,7 @@ diff --git a/client.js b/client.js const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; -@@ -11707,7 +18100,7 @@ +@@ -11707,7 +18108,7 @@ trail.cancel(); }; }); @@ -17266,7 +19459,7 @@ diff --git a/client.js b/client.js const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { -@@ -11721,13 +18114,13 @@ +@@ -11721,13 +18122,13 @@ observer.observe(box); return () => observer.disconnect(); }, []); @@ -17283,7 +19476,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -11737,13 +18130,13 @@ +@@ -11737,13 +18138,13 @@ title: bot.name, onClick: () => onView("details") }, @@ -17300,7 +19493,7 @@ diff --git a/client.js b/client.js "button", { key: tabId, -@@ -11764,9 +18157,9 @@ +@@ -11764,9 +18165,9 @@ } } }, @@ -17313,7 +19506,7 @@ diff --git a/client.js b/client.js className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), -@@ -11778,20 +18171,20 @@ +@@ -11778,20 +18179,20 @@ }, "×") ); }), @@ -17341,7 +19534,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,43 +18210,43 @@ +@@ -11817,43 +18218,43 @@ window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -17398,7 +19591,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +18270,25 @@ +@@ -11877,25 +18278,25 @@ } }) ), @@ -17434,7 +19627,7 @@ diff --git a/client.js b/client.js ) : null ) ); -@@ -11907,41 +18300,41 @@ +@@ -11907,41 +18308,41 @@ const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); @@ -17492,7 +19685,7 @@ diff --git a/client.js b/client.js let timer; const onResize = () => { setResizing(true); -@@ -11955,28 +18348,28 @@ +@@ -11955,28 +18356,28 @@ window.removeEventListener("resize", onResize); }; }, []); @@ -17528,7 +19721,7 @@ diff --git a/client.js b/client.js if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +18406,7 @@ +@@ -12013,7 +18414,7 @@ return actions[name](...args); }; } @@ -17537,7 +19730,7 @@ diff --git a/client.js b/client.js const move = fly.current; if (!move) return; fly.current = null; -@@ -12096,7 +18489,7 @@ +@@ -12096,7 +18497,7 @@ for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; @@ -17546,7 +19739,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +18506,15 @@ +@@ -12113,15 +18514,15 @@ if (event.target === event.currentTarget) setLanded(true); } }, @@ -17567,7 +19760,7 @@ diff --git a/client.js b/client.js sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, -@@ -12217,9 +18610,79 @@ +@@ -12217,9 +18618,79 @@ @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} `; @@ -17649,7 +19842,7 @@ diff --git a/client.js b/client.js var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -@@ -12227,15 +18690,15 @@ +@@ -12227,15 +18698,15 @@ const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); @@ -17669,7 +19862,7 @@ diff --git a/client.js b/client.js if (open) { setKept(open); return void 0; -@@ -12245,19 +18708,26 @@ +@@ -12245,19 +18716,26 @@ }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); const pane = open ?? kept; const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); @@ -17706,7 +19899,7 @@ diff --git a/client.js b/client.js ); } -@@ -12294,19 +18764,19 @@ +@@ -12294,19 +18772,19 @@ const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); }; @@ -17733,7 +19926,7 @@ diff --git a/client.js b/client.js const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); const childrenOf = (el) => { if (el.shadowRoot) return el.shadowRoot.childNodes; -@@ -12340,8 +18810,8 @@ +@@ -12340,8 +18818,8 @@ items.push(item); return { item, number: items.length, secret }; }; @@ -17744,7 +19937,7 @@ diff --git a/client.js b/client.js ).slice(0, 80); const fenced = (code, lang) => { const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); -@@ -12372,7 +18842,7 @@ +@@ -12372,7 +18850,7 @@ const marker = ordered ? `${number++}. ` : "- "; if (body2 === "") continue; const pad2 = " ".repeat(marker.length); @@ -17753,7 +19946,7 @@ diff --git a/client.js b/client.js } return keep(lines.join("\n")); }; -@@ -12398,11 +18868,11 @@ +@@ -12398,11 +18876,11 @@ let width = 0; for (const row of rows.slice(0, limits.tableRows + 1)) { if (over()) break; @@ -17767,7 +19960,7 @@ diff --git a/client.js b/client.js carry[column].left -= 1; column += 1; } -@@ -12410,18 +18880,18 @@ +@@ -12410,18 +18888,18 @@ for (const cell of row.cells) { place(); if (styleOf(cell).display === "none") continue; @@ -17791,7 +19984,7 @@ diff --git a/client.js b/client.js } width = Math.min(width, limits.tableCols); if (grid.length === 0 || width === 0) return caption ? ` -@@ -12480,9 +18950,9 @@ +@@ -12480,9 +18958,9 @@ if (node.nodeType === 3) { if (!ctx.shown) return ""; const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); @@ -17804,7 +19997,7 @@ diff --git a/client.js b/client.js } if (node.nodeType !== 1) return ""; const el = node; -@@ -12552,7 +19022,7 @@ +@@ -12552,7 +19030,7 @@ } if (tag === "blockquote") { const quoted = block(body2); @@ -17813,7 +20006,7 @@ diff --git a/client.js b/client.js } const end = body2.trimEnd().length; const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -@@ -12570,12 +19040,12 @@ +@@ -12570,12 +19048,12 @@ return marked; } const body = document.body; @@ -17830,7 +20023,7 @@ diff --git a/client.js b/client.js stop ||= `the text passed ${limits.chars} characters`; } const page = { -@@ -12585,7 +19055,7 @@ +@@ -12585,7 +19063,7 @@ description: document.querySelector('meta[name="description"]')?.content || "", selection: String(getSelection() || "").slice(0, 4e3), format: "markdown", @@ -17839,7 +20032,7 @@ diff --git a/client.js b/client.js items, stats: { ...stats, ms: Math.round(performance.now() - started) } }; -@@ -12613,15 +19083,15 @@ +@@ -12613,15 +19091,15 @@ var BLOCKED = "Cloud metadata addresses do not open here."; var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); function browserAddress(input) { @@ -17863,7 +20056,7 @@ diff --git a/client.js b/client.js } function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { const views = /* @__PURE__ */ new Map(); -@@ -13052,11 +19522,295 @@ +@@ -13052,11 +19530,295 @@ }; } @@ -18160,7 +20353,7 @@ diff --git a/client.js b/client.js function disposeAll(disposers, label) { for (const dispose of [...disposers].reverse()) { try { -@@ -13075,8 +19829,8 @@ +@@ -13075,8 +19837,8 @@ }; return { reconcile(wanted) { @@ -18171,7 +20364,7 @@ diff --git a/client.js b/client.js if (!wanted[group] || mounted[group].length > 0) continue; try { mounted[group] = install[group](); -@@ -13086,7 +19840,7 @@ +@@ -13086,7 +19848,7 @@ } }, dispose() { @@ -18180,7 +20373,7 @@ diff --git a/client.js b/client.js } }; } -@@ -13105,7 +19859,7 @@ +@@ -13105,7 +19867,7 @@ } // src/client/user-message.js @@ -18189,7 +20382,7 @@ diff --git a/client.js b/client.js var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { -@@ -13119,87 +19873,165 @@ +@@ -13119,87 +19881,165 @@ } return { text: texts2.join("\n"), attachments, rest }; } @@ -18238,8 +20431,7 @@ diff --git a/client.js b/client.js + return faceAliases(record, seat.bot).map((name) => ({ ...target, name, mentionLabel })); + }); + return bots.concat(humans); - } --function userTextParts(text, bots, referenceLabels = []) { ++} +function messageAuthor(data, roster, sessionId) { + const source = data?.source ?? {}; + const authorId = source.authorId; @@ -18254,7 +20446,8 @@ diff --git a/client.js b/client.js + ownerName: remote?.ownerName, + mine: roster?.self?.memberId === authorId + }; -+} + } +-function userTextParts(text, bots, referenceLabels = []) { +function userTextParts(text2, bots, referenceLabels = []) { const parts = []; let cursor = 0; @@ -18389,7 +20582,7 @@ diff --git a/client.js b/client.js .bt-ubody a{color:inherit} .bt-umention{display:inline-flex;align-items:center;gap:3px;vertical-align:-3px;max-width:100%;padding:0 1px;border:0;background:none;color:inherit;font:inherit;font-weight:600;line-height:inherit;cursor:pointer;border-radius:6px;white-space:nowrap} .bt-umention:hover{text-decoration:underline;text-underline-offset:2px} -@@ -13216,42 +20048,115 @@ +@@ -13216,42 +20056,115 @@ `; // src/client/approval-notices.js @@ -18486,7 +20679,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) { @@ -18496,7 +20689,7 @@ diff --git a/client.js b/client.js + return (0, import_react48.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 @@ -18518,7 +20711,7 @@ diff --git a/client.js b/client.js migrateStorage(); const UNREAD_KEY = STORAGE.unread; let savedUnread = {}; -@@ -13274,7 +20179,13 @@ +@@ -13274,7 +20187,13 @@ } catch { seenAt = {}; } @@ -18533,7 +20726,7 @@ diff --git a/client.js b/client.js const surface = createSource({ mode: readMode(), plain: false }); let agentSessionCache = /* @__PURE__ */ new Set(); try { -@@ -13308,6 +20219,8 @@ +@@ -13308,6 +20227,8 @@ return () => window.removeEventListener("pagehide", onPageHide); }, "ds-bot: remember visits on page hide"); const activity = createSource({}); @@ -18542,7 +20735,7 @@ diff --git a/client.js b/client.js const work = createSource({ bots: {}, approvals: [] }); let workKey = ""; const live = createSource({}); -@@ -13344,13 +20257,25 @@ +@@ -13344,13 +20265,25 @@ } return { ...state, unread }; }); @@ -18569,7 +20762,7 @@ diff --git a/client.js b/client.js if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); -@@ -13358,6 +20283,11 @@ +@@ -13358,6 +20291,11 @@ activityKey = nextActivity; activity.set(value.activity ?? {}); } @@ -18581,7 +20774,7 @@ diff --git a/client.js b/client.js const nextWork = JSON.stringify(value.work ?? null); if (nextWork !== workKey) { workKey = nextWork; -@@ -13368,10 +20298,15 @@ +@@ -13368,10 +20306,15 @@ const live2 = {}; if (value.connectors !== void 0 && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live2.connectors = value.connectors; if (value.capabilityStatuses !== void 0 && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live2.capabilityStatuses = value.capabilityStatuses; @@ -18597,7 +20790,7 @@ diff --git a/client.js b/client.js try { const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []); localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)); -@@ -13488,20 +20423,12 @@ +@@ -13488,20 +20431,12 @@ const style = installStyles("", "mentions"); let last = ""; const render = () => { @@ -18620,7 +20813,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13520,7 +20447,7 @@ +@@ -13520,7 +20455,7 @@ const accent = isHex(prefs.accent) ? prefs.accent : void 0; const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal"; document.body.dataset.btMotion = motion; @@ -18629,7 +20822,7 @@ diff --git a/client.js b/client.js if (next === key) return; key = next; style.textContent = themeCss(themes[id], accent); -@@ -13536,7 +20463,7 @@ +@@ -13536,7 +20471,7 @@ }; render(); const offRoster = roster.subscribe(render); @@ -18638,7 +20831,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13588,6 +20515,7 @@ +@@ -13588,6 +20523,7 @@ // or the click appears to do nothing. isAgentSession: (id) => isAgentSessionId(id), openSession: (id) => { @@ -18646,7 +20839,7 @@ diff --git a/client.js b/client.js if (isAgentSessionId(id)) return actions.openAgentSession(id); ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state); setPlain(false); -@@ -13653,12 +20581,12 @@ +@@ -13653,12 +20589,12 @@ }, // `last` is the Bot conversation the view held before it cleared: when that part // was archived for a fresh one, the view goes on in the fresh one. @@ -18662,7 +20855,7 @@ diff --git a/client.js b/client.js expected = target; ctx.uiWorkspace.openSession(target); }, -@@ -13708,10 +20636,15 @@ +@@ -13708,10 +20644,15 @@ }, // `id` opens one Bot (page 'bots') or one group chat (page 'groups'). openSettings: (page = "general", id = null) => { @@ -18680,7 +20873,7 @@ diff --git a/client.js b/client.js }, openMemory: (botId) => { ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null })); -@@ -13719,6 +20652,19 @@ +@@ -13719,6 +20660,19 @@ closeMemory: () => { ui2.set((state) => state.memory ? { ...state, memory: null } : state); }, @@ -18700,7 +20893,7 @@ diff --git a/client.js b/client.js openConnectors: () => { ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null })); }, -@@ -13728,7 +20674,7 @@ +@@ -13728,7 +20682,7 @@ if (mode !== "bot" && mode !== "agent") return; if (surface.getSnapshot().mode === mode) return; if (mode === "agent") { @@ -18709,7 +20902,7 @@ diff --git a/client.js b/client.js } writeMode(mode); surface.set((state) => ({ ...state, mode })); -@@ -13757,6 +20703,12 @@ +@@ -13757,6 +20711,12 @@ const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null); ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } })); }, @@ -18722,7 +20915,7 @@ diff --git a/client.js b/client.js endVisit: (id) => { leaveVisits([id]); ui2.set((state) => { -@@ -13780,7 +20732,7 @@ +@@ -13780,7 +20740,7 @@ } return { ...state, reactions }; }), @@ -18731,7 +20924,7 @@ diff --git a/client.js b/client.js clearQuote: (token) => ui2.set((state) => state.quote?.token === token ? { ...state, quote: null } : state), clearUnread: (id) => setUnread(id, false), setFlags: (id, flags) => after(call("set-flags", { id, ...flags })), -@@ -13808,10 +20760,16 @@ +@@ -13808,10 +20768,16 @@ setPrefs: (prefs) => after(call("set-prefs", prefs)), setMain: (id, main = true, options = {}) => after(call("set-main", { id, main, ...options })), updateRoom: (id, patch) => after(call("update-room", { id, ...patch })), @@ -18749,7 +20942,7 @@ diff --git a/client.js b/client.js stopMemberWork: (roomId, botId) => after(call("stop-member-work", { roomId, botId })), // Values go out in these requests and never come back in a response. secretSet: (payload) => after(call("secret-set", payload)), -@@ -13819,15 +20777,31 @@ +@@ -13819,15 +20785,31 @@ secretCancel: (requestId) => after(call("secret-cancel", { requestId })), secretUpdate: (name, scope) => after(call("secret-update", { name, scope })), secretDelete: (name) => after(call("secret-delete", { name })), @@ -18782,7 +20975,7 @@ diff --git a/client.js b/client.js scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }), exchange: (a, b) => call("exchange", { a, b }).catch(() => []), roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []), -@@ -13836,7 +20810,7 @@ +@@ -13836,7 +20818,7 @@ usage: () => call("usage", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), // `summarize` starts a summary when the entries changed; `regenerate` always does. memoryView: (botId, flags = {}) => call("memory-view", { botId, ...flags }), @@ -18791,7 +20984,7 @@ diff --git a/client.js b/client.js memoryForget: (botId, entry) => call("memory-forget", { botId, scope: entry.scope, topic: entry.topic, text: entry.text }), diagnostics: (payload) => call("diagnostics", payload), diagnosticsLog: () => call("diagnostics-log"), -@@ -13846,6 +20820,8 @@ +@@ -13846,6 +20828,8 @@ noteClientError(where, error); ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, @@ -18800,7 +20993,7 @@ diff --git a/client.js b/client.js dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { -@@ -13890,6 +20866,7 @@ +@@ -13890,6 +20874,7 @@ actions.closePanel = () => { ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state); }; @@ -18808,7 +21001,7 @@ diff --git a/client.js b/client.js actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null; actions.canSignIn = () => Boolean(actions.signInEntry()); actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 })); -@@ -13922,6 +20899,9 @@ +@@ -13922,6 +20907,9 @@ wakeUpdate(); }; actions.repoStars = () => call("repo-stars"); @@ -18818,7 +21011,7 @@ diff --git a/client.js b/client.js actions.noteTurnTime = (sessionId, turn, key, time) => { if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return; turnClock.set((state) => { -@@ -13964,7 +20944,13 @@ +@@ -13964,7 +20952,13 @@ }); const browserPages = browsers?.pages ?? createSource({}); const browserTabs = browsers?.tabs ?? createSource({}); @@ -18833,7 +21026,7 @@ diff --git a/client.js b/client.js ctx.effect(() => noticeApprovals({ work, roster, -@@ -13989,7 +20975,17 @@ +@@ -13989,7 +20983,17 @@ } }; }; const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component)); @@ -18851,7 +21044,7 @@ diff --git a/client.js b/client.js const installAtMenu = () => { const fiber = ctx.inject(["inputTriggers"], (scope) => { scope.effect(() => { -@@ -14001,7 +20997,12 @@ +@@ -14001,7 +21005,12 @@ const launched = (sessionId) => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE; const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map((source) => triggers.registerSource(source)); atMenu = { controllerOf }; @@ -18864,7 +21057,7 @@ diff --git a/client.js b/client.js atMenu = null; for (const off of offs) off(); }; -@@ -14010,6 +21011,14 @@ +@@ -14010,6 +21019,14 @@ return () => fiber.dispose(); }; actions.canReferenceChat = () => atMenu !== null; @@ -18879,7 +21072,7 @@ diff --git a/client.js b/client.js actions.referenceChat = (sessionId, span) => { const controller = atMenu?.controllerOf(sessionId); if (controller === void 0 || span === void 0) return; -@@ -14019,33 +21028,49 @@ +@@ -14019,33 +21036,49 @@ installCompiledStyles(SHELL_CSS, "shell"), installCompiledStyles(SIDEBAR_CSS, "sidebar"), installCompiledStyles(SECRET_CSS, "secrets"), @@ -18933,7 +21126,7 @@ diff --git a/client.js b/client.js shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell), shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell), shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell), -@@ -14057,15 +21082,24 @@ +@@ -14057,15 +21090,24 @@ // every message. shadow("conversation.chat.node", { key: "question-reply", priority: -10, locale: "chat" }, AnsweredCell), shadow("conversation.chat.node", { key: "compaction", priority: -10, locale: "chat" }, Nothing), @@ -18961,7 +21154,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 +21108,18 @@ +@@ -14074,8 +21116,17 @@ 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), @@ -18969,7 +21162,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_react49.createElement)(TurnFailures, { ...props, work }) }); + return () => { @@ -18977,7 +21170,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/inbox.js b/patches/ds-bot/files/src/client/inbox.js index 68666c405..df29c687b 100644 --- a/patches/ds-bot/files/src/client/inbox.js +++ b/patches/ds-bot/files/src/client/inbox.js @@ -17,6 +17,7 @@ import { alwaysChoices } from './member-status.js' import { botNames, currentPart, previewOf, relativeShort } from './text.js' import { hostText, t } from './i18n.js' import { failureSentence } from '../shared/failure-copy.js' +import { columnOf } from '../shared/task-board.js' // Done (and a failed task) ages out after 7 days. export const KEEP_MS = 7 * 86_400_000 @@ -292,8 +293,13 @@ export function inboxModel({ roster, work = {}, tasks = [], sessions, workflows if (task.status === 'failed') { if (fresh(task.updatedAt)) needs.push({ ...card, status: 'failed', reason: lastWhy(task) }) } else if (task.status === 'result-ready') needs.push({ ...card, status: 'result', reason: task.result?.summary ?? '' }) else if (task.status === 'blocked') needs.push({ ...card, status: task.waitingFor === 'input' ? 'input' : 'approval', reason: task.inputRequest?.question ?? '' }) - else if (task.status === 'open' || task.status === 'claimed' || task.status === 'working') progress.push({ ...card, status: 'task', since: task.createdAt }) - else if ((task.status === 'accepted' || task.status === 'cancelled') && fresh(task.updatedAt)) done.push({ ...card, status: task.status === 'accepted' ? 'done' : 'cancelled' }) + else { + // One mapping with the board (#1739): the column is the task row's own status, so the + // Inbox never gives a row a state the board does not give it. + 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' }) + } } // A workflow run that failed (#1706). The plugin keeps that failure in its own // state, so the Inbox reads it from the same pack the Workflows page gets. One @@ -358,6 +364,13 @@ export const needsCount = (roster, work, tasks, now, workflows = []) => inboxMod // Cards the owner already answered leave the Needs you list. The dialog keeps that // set until the next roster poll drops the underlying record. +// A task card's age is the row's own clock (#1739): the moment the row entered working, +// never the wall clock and never its creation, so a row that never started carries no age. +function startedAtOf(task) { + const started = (task.actors ?? []).filter(actor => actor.status === 'working' && typeof actor.at === 'number') + return started.length ? started.at(-1).at : task.updatedAt +} + export function visibleNeeds(model, answered) { return { ...model, needs: model.needs.filter(card => !answered?.[card.id]) } } diff --git a/patches/ds-bot/files/test/inbox-task-status.test.js b/patches/ds-bot/files/test/inbox-task-status.test.js new file mode 100644 index 000000000..132d7ce24 --- /dev/null +++ b/patches/ds-bot/files/test/inbox-task-status.test.js @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' +import { columnOf } from '../src/shared/task-board.js' + +// #1739: the Inbox and the Board must read one task row the same way. The row's status is the +// only source; neither view derives a state from session or turn recency, and a card's age is +// the row's own started-at, never its creation — a row that never started has no age. +const bundle = await build({ + entryPoints: [fileURLToPath(new URL('../src/client/inbox.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout: () => 0, clearTimeout: () => {}, setInterval: () => 0, clearInterval: () => {}, Date, Promise, + require: (name) => { + const platform = { + react: { + createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }), + useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}], + useEffect: () => {}, useLayoutEffect: () => {}, useRef: current => ({ current }), useCallback: fn => fn, useMemo: fn => fn(), Fragment: 'fragment', + }, + 'react-dom': {}, + '@deepseek-ai/dsh-client-ui-primitives': {}, + } + if (!(name in platform)) throw new Error(`no platform module ${name}`) + return platform[name] + }, +}) +const { inboxModel } = module.exports + +const NOW = Date.UTC(2026, 10, 29, 23, 0, 0) +const HOUR = 3_600_000 +const roster = { + self: { memberId: 'm1' }, + bots: [{ id: 'b1', name: 'Chief', ownerId: 'm1', sessionId: 's1' }], + rooms: [], +} +roster.byId = Object.fromEntries(roster.bots.map(bot => [bot.id, bot])) +roster.roomsById = {} +const row = (id, status, extra = {}) => ({ + id, status, title: `Task ${id}`, ownerBotId: 'b1', requesterId: 'm1', origin: {}, + createdAt: NOW - 63 * HOUR, updatedAt: NOW - 63 * HOUR, actors: [], ...extra, +}) +// The reported set: five rows that were never claimed. The board paints them Not started (0/5). +const neverStarted = ['t1', 't2', 't3', 't4', 't5'].map(id => row(id, 'open')) +const claimed = row('t6', 'claimed', { actors: [{ status: 'claimed', at: NOW - 63 * HOUR }] }) +const working = row('t7', 'working', { + createdAt: NOW - 70 * HOUR, + updatedAt: NOW - 2 * HOUR, + actors: [{ status: 'claimed', at: NOW - 70 * HOUR }, { status: 'working', at: NOW - 63 * HOUR }], +}) +const workingNoLog = row('t8', 'working', { createdAt: NOW - 70 * HOUR, updatedAt: NOW - 3 * HOUR }) +const accepted = row('t9', 'accepted', { updatedAt: NOW - HOUR }) +const tasks = [...neverStarted, claimed, working, workingNoLog, accepted] +const model = inboxModel({ roster, work: {}, tasks, sessions: { byId: {} }, now: NOW }) +const ids = list => Array.from(list, card => String(card.id)) + +test('a row the board paints Not started is not an In progress card in the Inbox (#1739)', () => { + // The reported defect: Board 0/5 Not started while the Inbox showed the same five rows as + // In progress for 63h. + assert.deepEqual(ids(model.progress), ['task:t7', 'task:t8']) + for (const task of [...neverStarted, claimed, accepted]) { + assert.ok(!ids(model.progress).includes(`task:${task.id}`), `${task.id} (${task.status}) must not be In progress`) + } +}) + +test('both views read the same row the same way: In progress is exactly the board in-progress column', () => { + const boardInProgress = tasks.filter(task => columnOf(task) === 'in-progress').map(task => `task:${task.id}`) + assert.deepEqual(ids(model.progress), boardInProgress) + const boardNotStarted = tasks.filter(task => columnOf(task) === 'not-started') + assert.equal(boardNotStarted.length, 6) + // The Inbox shows no card at all for those rows, so it can show no age for them either. + assert.deepEqual(ids(model.progress).filter(id => boardNotStarted.some(task => `task:${task.id}` === id)), []) +}) + +test('a card age comes from the row clock: the moment it started, never its creation', () => { + const card = model.progress.find(entry => entry.id === 'task:t7') + assert.equal(card.since, NOW - 63 * HOUR) + const fallback = model.progress.find(entry => entry.id === 'task:t8') + assert.equal(fallback.since, NOW - 3 * HOUR, 'a working row with no transition log ages from its own updatedAt') + for (const entry of model.progress) assert.ok(typeof entry.since === 'number' && entry.since <= NOW) +}) From d3333e124c8d668633d2bfd00ebc1b706dac9536 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Fri, 30 Oct 2026 08:10:29 +0800 Subject: [PATCH 2/3] fix: keep the changelog fragment to its entry text The fragment is assembled behind a bullet, so it carries no leading "- " (#1739). Docs-not-needed: the Inbox and the Board now share the existing task-row mapping; no subsystem contract changes, only the client rendering of it. --- changelog.d/1739-task-status-one-source.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1739-task-status-one-source.fixed.md b/changelog.d/1739-task-status-one-source.fixed.md index d05d7a602..18dd51f2f 100644 --- a/changelog.d/1739-task-status-one-source.fixed.md +++ b/changelog.d/1739-task-status-one-source.fixed.md @@ -1 +1 @@ -- The Inbox no longer shows a task that was never started as "In progress", and no longer ages it from its creation: the Inbox and the Board now read one task row, so the status label and the counter agree and a card's age comes from the moment the row started. +The Inbox no longer shows a task that was never started as "In progress", and no longer ages it from its creation: the Inbox and the Board now read one task row, so the status label and the counter agree and a card's age comes from the moment the row started. From 55b1100c0e8368cbd8f639c449445986cc59e963 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Fri, 30 Oct 2026 08:14:40 +0800 Subject: [PATCH 3/3] fix: close the client bundle patch with its last context line The generated 0008 patch ended two lines early, so a strict applier read blank lines as hunk lines (#1739). Docs-not-needed: the Inbox and the Board now share the existing task-row mapping; no subsystem contract changes, only the client rendering of it. --- patches/ds-bot/0008-client-bundle.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index f64d2f290..62853161f 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -21154,7 +21154,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 +21116,17 @@ +@@ -14074,9 +21116,18 @@ 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), @@ -21173,4 +21173,4 @@ diff --git a/client.js b/client.js ctx.effect(() => { const style = installStyles(RETURN_CSS, "return"); return () => style.remove(); - + }, "ds-bot: return styles");