From 166418c1e4f4814ac31c1241e14fe5a5c529583f Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Mon, 26 Oct 2026 13:29:05 +0800 Subject: [PATCH] feat(delivery): draw a seven-day line for each delivery metric (#1212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner: Dev4 Written-by: Dev4 Reviewer: Reviewer metrics.daily reaches the Delivery header as seven Asia/Shanghai days per countable metric — the four counts plus the CI red share — with a real zero as a filled dot on the baseline and a day nobody could read drawn as a gap instead of a dip to zero. The running day is the one hollow point. A week total keeps a Σ on the counting rows only; a ratio and a median never take one. Rebased onto 00bd3820. #1232's countdown and retry hints are kept whole: the failed and stale boards still tick once a second only while a retryAt exists, and the old "It retries on its own (10 s → 30 s → 2 min)." ZH sentence is gone. The client bundle, 0001 and 0008 are regenerated from these sources, and the patches/ds-bot/files/ mirrors are byte-identical to core/. --- changelog.d/1212-delivery-charts.added.md | 1 + core/client.js | 3952 ++--- core/src/client/delivery-chart.js | 243 + core/src/client/delivery-view.js | 25 +- core/src/client/i18n.js | 10 + core/test/delivery-chart.test.js | 236 + core/test/delivery-view.test.js | 42 +- docs/subsystems/core-client.md | 1 + patches/ds-bot/0001-core-fixes.patch | 62 +- patches/ds-bot/0008-client-bundle.patch | 12214 +++++++++++----- .../ds-bot/files/src/client/delivery-chart.js | 243 + .../ds-bot/files/src/client/delivery-view.js | 25 +- .../ds-bot/files/test/delivery-chart.test.js | 236 + .../ds-bot/files/test/delivery-view.test.js | 42 +- 14 files changed, 11401 insertions(+), 5931 deletions(-) create mode 100644 changelog.d/1212-delivery-charts.added.md create mode 100644 core/src/client/delivery-chart.js create mode 100644 core/test/delivery-chart.test.js create mode 100644 patches/ds-bot/files/src/client/delivery-chart.js create mode 100644 patches/ds-bot/files/test/delivery-chart.test.js diff --git a/changelog.d/1212-delivery-charts.added.md b/changelog.d/1212-delivery-charts.added.md new file mode 100644 index 00000000..4e7766ca --- /dev/null +++ b/changelog.d/1212-delivery-charts.added.md @@ -0,0 +1 @@ +The delivery board draws the last seven days instead of two numbers per metric. New issues, Closed issues, New pull requests, Merged pull requests and the CI red share all get a 168×28 line beside the existing **Today** / **7 days** columns, where a count's week number is a Σ total and the CI share, a ratio, never takes one. Every point carries a title, a day that could not be read breaks the line instead of reading as zero, and a total over an incomplete window wears a star the footnote explains. Below 900px the line moves under its row, and a snapshot without the series says so in one line instead of drawing empty charts. ([#1212](https://github.com/ThinkFlowLab/FlyBot/issues/1212)) diff --git a/core/client.js b/core/client.js index 498ff370..222dd883 100644 --- a/core/client.js +++ b/core/client.js @@ -1373,6 +1373,16 @@ var HOST_ZH = { "median open age {age}": "开着的单中位 {age}", "as of {time}": "截至 {time}", "refreshing…": "刷新中…", + // #1212: the 7-day lines. A day the host could not read, and a snapshot with no series at all. + "not available": "未取到", + "No 7-day data yet.": "7 天数据未取到", + "{label} over the last 7 days": "{label}近 7 天", + "Last 7 days": "近 7 天", + "so far": "截至现在", + "today is partial · as of {time}": "今天为部分日 · 截至 {time}", + "* {count} of 7 days had data": "* 7 天中 {count} 天有数", + "* {count} of {days} days had data": "* {days} 天中 {count} 天有数", + "CI red share": "CI 红占比", "No snapshot yet.": "还没有快照。", "Nothing here.": "这里还没有。", "First load in progress, {seconds} s elapsed": "首次加载中,已 {seconds} 秒", @@ -2135,8 +2145,8 @@ function bodyOf(def, paint, flat = false) { return ""; } } -var eyeMarkup = ([cx, cy, w, h46, tilt]) => ``; -var happyEyeMarkup = ([cx, cy, w, h46, tilt]) => ``; +var eyeMarkup = ([cx, cy, w, h47, tilt]) => ``; +var happyEyeMarkup = ([cx, cy, w, h47, tilt]) => ``; var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`; var holes = (def, eyes) => `${eyes}`; var MASK_ID = "%MASK%"; @@ -2832,29 +2842,29 @@ function inviteCountdown(expiresAt, offsetMs = 0) { const total = Math.max(1, Math.ceil(left / 1e3)); return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`; } -function RoomInvitePreview({ h: h46, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { - if (!preview || typeof h46 !== "function") return null; +function RoomInvitePreview({ h: h47, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { + if (!preview || typeof h47 !== "function") return null; const at = `${kind}`; const child = (name) => `${at}-${name}`; const countdown = now === void 0 ? inviteCountdown(preview.expiresAt, preview.clockOffsetMs) : inviteCountdownAt(preview.expiresAt, preview.clockOffsetMs, now); - return h46( + return h47( "div", { className: "fs-preview", "data-flybot": at, role: "status" }, - h46("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), - preview.lead ? h46("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, - preview.people ? h46("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, - preview.expiresAt ? h46("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, - onJoin || onClear ? h46( + h47("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), + preview.lead ? h47("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, + preview.people ? h47("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, + preview.expiresAt ? h47("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, + onJoin || onClear ? h47( "div", { className: "fs-preview-foot", "data-flybot": child("foot") }, - showClear && onClear ? h46("button", { + showClear && onClear ? h47("button", { type: "button", className: "fs-btn fs-btn-sm", "data-flybot": child("clear"), disabled: !!busy, onClick: onClear }, clearLabel) : null, - onJoin ? h46("button", { + onJoin ? h47("button", { type: "button", className: "fs-btn fs-btn-sm fs-btn-primary", "data-flybot": child("join"), @@ -3419,12 +3429,12 @@ function liveSprings(apply2) { const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); last = now; const steps = Math.ceil(dt * 240); - const h46 = dt / steps; + const h47 = 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) * h46; - value.x += value.v * h46; + value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h47; + value.x += value.v * h47; } if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { value.x = value.to; @@ -8029,8 +8039,202 @@ function progressOf(task) { return Math.max(0, Math.min(100, Math.round(value))); } -// src/client/delivery-view.js +// src/client/delivery-chart.js var import_react14 = require("react"); +var CHART_WIDTH = 168; +var CHART_HEIGHT = 28; +var PAD = 3; +var DOT_RADIUS = 2; +var HOLLOW_RADIUS = 2.5; +var CHART_DAYS = 7; +var CHART_KEYS = ["newIssues", "closedIssues", "newPullRequests", "mergedPullRequests", "ci"]; +var COUNTING_KEYS = ["newIssues", "closedIssues", "newPullRequests", "mergedPullRequests"]; +function chartDays(daily) { + if (!Array.isArray(daily)) return null; + const days = daily.filter((day) => day && typeof day === "object"); + return days.length ? days.slice(-CHART_DAYS) : null; +} +function todayPartial(daily) { + const days = chartDays(daily); + return Boolean(days && days.length && days[days.length - 1].partial === true); +} +function dayValue(day, key) { + if (!day || typeof day !== "object") return null; + if (key === "ci") { + const failed = day.ci?.failed; + const counted = day.ci?.counted; + const total = day.ci?.total; + const denominator = Number.isFinite(counted) && counted > 0 ? counted : Number.isFinite(total) && total > 0 ? total : null; + if (!Number.isFinite(failed) || denominator === null) return null; + return Math.min(1, Math.max(0, failed / denominator)); + } + const value = day[key]; + return Number.isFinite(value) ? value : null; +} +function dayLabel2(day) { + if (typeof day?.date === "string" && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(day.date)) return day.date; + const at = Date.parse(day?.start ?? ""); + if (!Number.isFinite(at)) return ""; + return new Date(at + 8 * 36e5).toISOString().slice(0, 10); +} +function pointTitle(day, key) { + const date = dayLabel2(day); + const value = dayValue(day, key); + const shown = value === null ? t("not available") : key === "ci" ? `${Math.round(value * 100)}%` : String(value); + return date ? `${date} · ${shown}` : shown; +} +function knownDays(daily, key) { + const days = chartDays(daily) ?? []; + return days.filter((day) => dayValue(day, key) !== null).length; +} +var round = (value) => Math.round(value * 100) / 100; +function chartGeometry(daily, key, { width = CHART_WIDTH, height = CHART_HEIGHT } = {}) { + const days = chartDays(daily); + if (!days) return null; + const values = days.map((day) => dayValue(day, key)); + const known = values.filter((value) => value !== null); + const top = known.length ? Math.max(...known, 0) : 0; + const span = top > 0 ? top : 1; + const step = days.length > 1 ? (width - PAD * 2) / (days.length - 1) : 0; + const dots = days.map((day, i) => { + const value = values[i]; + const last = i === days.length - 1; + const state = value === null ? "missing" : last && day.partial === true ? "partial" : "full"; + return { + index: i, + x: round(PAD + step * i), + y: value === null ? null : round(PAD + (height - PAD * 2) * (1 - value / span)), + today: last, + // Only the day that is not over yet is drawn hollow; a finished day is a filled dot. + hollow: state === "partial", + partial: state === "partial", + state, + title: pointTitle(day, key), + value + }; + }); + const segments = []; + let run = null; + for (const dot of dots) { + if (dot.y === null) { + if (run) { + segments.push(run); + run = null; + } + continue; + } + if (!run) run = { dots: [] }; + run.dots.push(dot); + } + if (run) segments.push(run); + return { + width, + height, + dots, + known: known.length, + segments: segments.map((segment) => ({ + d: segment.dots.map((dot, i) => `${i ? "L" : "M"}${dot.x} ${dot.y}`).join(" "), + // Only the stretch that ends today is the day that is not over yet. + dashed: segment.dots.some((dot) => dot.partial), + today: segment.dots.some((dot) => dot.today) + })) + }; +} +function DeliveryChart({ daily, metricKey, label }) { + const geo = chartGeometry(daily, metricKey); + if (!geo || !geo.known) return null; + const name = label ? t("{label} over the last 7 days", { label }) : t("Last 7 days"); + return (0, import_react14.createElement)( + "svg", + { + className: "bt-del-chart", + "data-flybot": "delivery-chart", + "data-metric": metricKey, + width: geo.width, + height: geo.height, + viewBox: `0 0 ${geo.width} ${geo.height}`, + role: "img", + "aria-label": name + }, + geo.segments.map((segment, i) => (0, import_react14.createElement)("path", { + key: `s${i}`, + className: "bt-del-chart-line", + "data-today": segment.today ? "true" : void 0, + ...segment.dashed ? { "stroke-dasharray": "2 2" } : {}, + d: segment.d + })), + geo.dots.filter((dot) => dot.y !== null).map((dot) => (0, import_react14.createElement)("circle", { + key: `d${dot.index}`, + className: "bt-del-chart-dot", + "data-state": dot.state, + "data-hollow": dot.hollow ? "true" : void 0, + cx: dot.x, + cy: dot.y, + r: dot.hollow ? HOLLOW_RADIUS : DOT_RADIUS + }, (0, import_react14.createElement)("title", null, dot.title))) + ); +} +function DeliveryMetrics({ rows = [], daily }) { + const days = chartDays(daily); + const short = CHART_KEYS.map((key) => knownDays(days, key)).filter((count) => count < (days?.length ?? 0)); + const fewest = short.length ? Math.min(...short) : 0; + const head = (0, import_react14.createElement)( + "div", + { className: "bt-del-dm-head", "data-flybot": "delivery-metrics-head" }, + (0, import_react14.createElement)("span", { className: "bt-del-dm-label" }, ""), + (0, import_react14.createElement)("span", { className: "bt-del-dm-chart-head" }, ""), + (0, import_react14.createElement)("span", { className: "bt-del-dm-value" }, t("Today")), + (0, import_react14.createElement)("span", { className: "bt-del-dm-value bt-del-dm-week" }, t("7 days")) + ); + const body = rows.map((row, index) => { + const key = CHART_KEYS.includes(row?.key) ? row.key : null; + const chart = days && key ? (0, import_react14.createElement)(DeliveryChart, { key: `c-${key}`, daily: days, metricKey: key, label: row?.label }) : null; + const counted = key !== null && COUNTING_KEYS.includes(key); + const star = days && counted && knownDays(days, key) < days.length ? "*" : ""; + const cells = [ + (0, import_react14.createElement)("span", { key: "label", className: "bt-del-dm-label" }, row?.label ?? ""), + // A median row gets no chart cell at all: no line, no placeholder box, no total. + ...key ? [(0, import_react14.createElement)("span", { key: "chart", className: "bt-del-dm-chart" }, chart)] : [], + (0, import_react14.createElement)("span", { key: "today", className: "bt-del-dm-value bt-del-dm-today" }, row?.today ?? "—"), + (0, import_react14.createElement)("span", { key: "week", className: "bt-del-dm-value bt-del-dm-week" }, counted ? `Σ ${row?.week ?? "—"}${star}` : row?.week ?? "—") + ]; + return (0, import_react14.createElement)("div", { key: row?.label ?? String(index), className: "bt-del-dm-row", "data-metric": key ?? void 0 }, cells); + }); + return (0, import_react14.createElement)( + "div", + { className: "bt-del-dm", "data-flybot": "delivery-metrics" }, + head, + body, + days ? null : (0, import_react14.createElement)("p", { className: "bt-del-chart-empty", "data-flybot": "delivery-chart-empty" }, t("No 7-day data yet.")), + days && short.length ? (0, import_react14.createElement)("p", { className: "bt-del-chart-foot", "data-flybot": "delivery-chart-foot" }, t("* {count} of {days} days had data", { count: fewest, days: days.length })) : null + ); +} +var DELIVERY_CHART_CSS = ` +.bt-del-dm{display:flex;flex-direction:column;gap:2px;min-width:0} +.bt-del-dm-head,.bt-del-dm-row{display:flex;align-items:center;gap:8px} +.bt-del-dm-head{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)} +.bt-del-dm-label{flex:1 1 auto;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-2)} +.bt-del-dm-head .bt-del-dm-label{color:inherit} +.bt-del-dm-chart,.bt-del-dm-chart-head{flex:0 0 168px;display:flex;align-items:center;min-height:28px} +.bt-del-dm-value{flex:0 0 auto;min-width:56px;text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;color:var(--bt-ink)} +.bt-del-dm-head .bt-del-dm-value{color:inherit;font-weight:inherit} +.bt-del-dm-today{color:var(--bt-ink);font-weight:600} +.bt-del-dm-week{color:var(--bt-ink-3);font-weight:400} +.bt-del-chart{display:block;overflow:visible} +.bt-del-chart-line{fill:none;stroke:var(--bt-accent);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round} +.bt-del-chart-dot{fill:var(--bt-accent);stroke:none} +.bt-del-chart-dot[data-state=partial]{fill:none;stroke:var(--bt-accent);stroke-width:1.5} +.bt-del-chart-empty,.bt-del-chart-foot{margin:2px 0 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} +@media (max-width:899.98px){ + .bt-del-dm-head{display:none} + .bt-del-dm-row{flex-wrap:wrap;gap:4px 8px;min-height:44px;align-content:center} + .bt-del-dm-chart{order:5;flex:0 0 100%;min-height:0} + .bt-del-dm-value{min-width:0} +} +`; + +// src/client/delivery-view.js +var import_react15 = require("react"); var POLL_MS3 = 6e4; var LOADING_POLL_MS = 2e3; var RETRY_FLOOR_MS = 1e3; @@ -8119,17 +8323,20 @@ function metricRows(snapshot) { const today = snapshot?.today ?? {}; const week = snapshot?.week ?? {}; const ci = today.ci ?? null; + const share = (entry) => entry && Number.isFinite(entry.counted) && entry.counted > 0 ? `${Math.round(entry.failed / entry.counted * 100)}%` : "—"; const count = (value) => value === null || value === void 0 ? "—" : String(value); const rows = [ { label: t("New issues"), key: "newIssues" }, { label: t("Closed issues"), key: "closedIssues" }, { label: t("New pull requests"), key: "newPullRequests" }, { label: t("Merged pull requests"), key: "mergedPullRequests" }, + { key: "ci", label: t("CI red share"), today: share(today.ci), week: share(week.ci) }, { label: t("Median issue close"), key: "medianIssueCloseMs", format: duration }, { label: t("Median PR merge"), key: "medianPullMergeMs", format: duration } ].map((row) => { + if (row.today !== void 0) return { key: row.key, label: row.label, today: row.today, week: row.week }; const format = row.format ?? count; - return { label: row.label, today: format(today[row.key]), week: format(week[row.key]) }; + return { key: row.key, label: row.label, today: format(today[row.key]), week: format(week[row.key]) }; }); return { dayStart: snapshot?.dayStart ?? null, @@ -8206,10 +8413,10 @@ function createPoller({ load, onView, now = () => Date.now(), setTimer = (fn, ms }; } function useDeliveryBoard(actions) { - const [view, setView] = (0, import_react14.useState)({ state: "idle" }); - const [retry, setRetry] = (0, import_react14.useState)(() => () => { + const [view, setView] = (0, import_react15.useState)({ state: "idle" }); + const [retry, setRetry] = (0, import_react15.useState)(() => () => { }); - (0, import_react14.useEffect)(() => { + (0, import_react15.useEffect)(() => { let current = { state: "idle" }; const poller = createPoller({ load: (force) => typeof actions?.deliveryBoard === "function" ? actions.deliveryBoard(force ? { force: true } : {}) : Promise.resolve(null), @@ -8235,8 +8442,8 @@ function probeText(report) { return lines.join("\n"); } function Diagnose({ onProbe }) { - const [report, setReport] = (0, import_react14.useState)(null); - const [busy, setBusy] = (0, import_react14.useState)(false); + const [report, setReport] = (0, import_react15.useState)(null); + const [busy, setBusy] = (0, import_react15.useState)(false); const run = () => { if (busy) return; setBusy(true); @@ -8244,15 +8451,15 @@ function Diagnose({ onProbe }) { setReport(next ?? { rows: [], error: "no answer" }); }).catch((error) => setReport({ rows: [], error: String(error?.message ?? error) })).finally(() => setBusy(false)); }; - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "span", { className: "bt-del-diag" }, - (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: run, "aria-label": t("Diagnose") }, busy ? t("Checking…") : t("Diagnose")), - report ? (0, import_react14.createElement)( + (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: run, "aria-label": t("Diagnose") }, busy ? t("Checking…") : t("Diagnose")), + report ? (0, import_react15.createElement)( "div", { className: "bt-del-diag-box" }, - (0, import_react14.createElement)("pre", { className: "bt-del-diag-text", "data-probe": "report" }, probeText(report)), - (0, import_react14.createElement)("button", { + (0, import_react15.createElement)("pre", { className: "bt-del-diag-text", "data-probe": "report" }, probeText(report)), + (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { @@ -8262,36 +8469,26 @@ function Diagnose({ onProbe }) { } } }, t("Copy")), - (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setReport(null) }, t("Close")) + (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setReport(null) }, t("Close")) ) : null ); } function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, onProbe = null }) { const payload = view ?? snapshot; if (!payload) return null; - const { rows, ci, open } = metricRows(payload.metrics ?? payload); + const metricsPayload = payload.metrics ?? payload; + const { rows, ci, open } = metricRows(metricsPayload); const error = payload.error ?? null; const state = payload.state ?? "fresh"; - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "header", { className: "bt-del-head" }, - (0, import_react14.createElement)( + (0, import_react15.createElement)( "div", { className: "bt-del-metrics" }, - (0, import_react14.createElement)( - "table", - { className: "bt-del-table" }, - (0, import_react14.createElement)("thead", null, (0, import_react14.createElement)("tr", null, (0, import_react14.createElement)("th", null, ""), (0, import_react14.createElement)("th", null, t("Today")), (0, import_react14.createElement)("th", null, t("7 days")))), - (0, import_react14.createElement)("tbody", null, rows.map((row) => (0, import_react14.createElement)( - "tr", - { key: row.label }, - (0, import_react14.createElement)("th", { scope: "row" }, row.label), - (0, import_react14.createElement)("td", null, row.today), - (0, import_react14.createElement)("td", null, row.week) - ))) - ), + (0, import_react15.createElement)(DeliveryMetrics, { rows, daily: metricsPayload.daily }), // An unreadable CI number is "unknown", never "0 of 0" (#1211). - (0, import_react14.createElement)( + (0, import_react15.createElement)( "p", { className: "bt-del-ci", "data-ci": ci === null ? "unknown" : "read" }, ci === null ? t("CI not available this time") : t("CI failed {failed} of {total}", { failed: ci.failed, total: ci.total }), @@ -8299,20 +8496,21 @@ function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, onProbe = ci !== null && ci.superseded ? ` · ${t("{count} superseded", { count: ci.superseded })}` : "", ci !== null && ci.unknownDays?.length ? ` · ${t("{count} day(s) not read", { count: ci.unknownDays.length })}` : "" ), - (0, import_react14.createElement)("p", { className: "bt-del-open" }, t("Open: {issues} issues ({needs} need a human), {prs} pull requests", { + (0, import_react15.createElement)("p", { className: "bt-del-open" }, t("Open: {issues} issues ({needs} need a human), {prs} pull requests", { issues: open.issues ?? 0, needs: open.needsHuman ?? 0, prs: open.pullRequests ?? 0 }), open.medianIssueAgeMs !== null && open.medianIssueAgeMs !== void 0 ? ` · ${t("median open age {age}", { age: duration(open.medianIssueAgeMs) })}` : "") ), - (0, import_react14.createElement)( + (0, import_react15.createElement)( "p", { className: "bt-del-asof" }, state === "loading" ? loadingText(payload.elapsedMs) : t("as of {time}", { time: timeOf(payload.asOf) }), + state !== "loading" && todayPartial(metricsPayload.daily) ? ` · ${t("today is partial · as of {time}", { time: clockOf(payload.asOf) })}` : "", payload.refreshing ? ` · ${t("refreshing…")}` : "", error ? ` · ${hintFor(error, { repo })}` : "", - onRetry ? (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null, - onProbe ? (0, import_react14.createElement)(Diagnose, { onProbe }) : null + onRetry ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null, + onProbe ? (0, import_react15.createElement)(Diagnose, { onProbe }) : null ) ); } @@ -8321,8 +8519,8 @@ function DeliveryBoard({ view, snapshot, message = "", repo = null, onRetry = nu const state = payload.state ?? "fresh"; const retryAt = Number(payload.retryAt); const ticking = (state === "failed" || state === "stale") && Number.isFinite(retryAt) && retryAt > 0; - const [now, setNow] = (0, import_react14.useState)(() => Date.now()); - (0, import_react14.useEffect)(() => { + const [now, setNow] = (0, import_react15.useState)(() => Date.now()); + (0, import_react15.useEffect)(() => { if (!ticking) return void 0; setNow(Date.now()); const timer = setInterval(() => setNow(Date.now()), 1e3); @@ -8330,74 +8528,74 @@ function DeliveryBoard({ view, snapshot, message = "", repo = null, onRetry = nu }, [ticking, retryAt]); const countdown = retryCountdown(payload.retryAt, now); if (state === "idle") { - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "div", { className: "bt-del-body", "data-view": "delivery", "data-state": state }, - message ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("No snapshot yet.")) + message ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("No snapshot yet.")) ); } if (state === "loading") { - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "div", { className: "bt-del-body", "data-view": "delivery", "data-state": state }, - (0, import_react14.createElement)("p", { className: "bt-del-loading", role: "status" }, loadingText(payload.elapsedMs)), - (0, import_react14.createElement)("p", { className: "bt-del-hint" }, t("The first round reads the repository once; the columns appear as soon as it lands.")) + (0, import_react15.createElement)("p", { className: "bt-del-loading", role: "status" }, loadingText(payload.elapsedMs)), + (0, import_react15.createElement)("p", { className: "bt-del-hint" }, t("The first round reads the repository once; the columns appear as soon as it lands.")) ); } if (state === "failed" || !payload.columns) { const error = payload.error ?? null; - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "div", { className: "bt-del-body", "data-view": "delivery", "data-state": state }, - (0, import_react14.createElement)( + (0, import_react15.createElement)( "div", { className: "bt-board-error", role: "alert" }, - (0, import_react14.createElement)("p", null, String(error?.message ?? message ?? t("The delivery board is not ready yet."))), - error ? (0, import_react14.createElement)("p", { className: "bt-del-hint" }, hintFor(error, { repo })) : null, - (0, import_react14.createElement)( + (0, import_react15.createElement)("p", null, String(error?.message ?? message ?? t("The delivery board is not ready yet."))), + error ? (0, import_react15.createElement)("p", { className: "bt-del-hint" }, hintFor(error, { repo })) : null, + (0, import_react15.createElement)( "p", { className: "bt-del-hint", "data-retry-at": payload.retryAt ?? void 0 }, countdown === null ? t("While this page is open it retries on its own; press Retry to try again now.") : t("Retrying in {seconds} s · press Retry to try again now", { seconds: countdown }) ), - onRetry ? (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null + onRetry ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null ) ); } const columns = payload.columns ?? {}; const missing = payload.missing ?? []; - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "div", { className: "bt-del-body", "data-view": "delivery", "data-state": state }, - message ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : null, - state === "stale" && payload.error ? (0, import_react14.createElement)( + message ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : null, + state === "stale" && payload.error ? (0, import_react15.createElement)( "p", { className: "bt-del-stale", role: "status" }, staleText(payload.asOf, payload.error), ` · ${hintFor(payload.error, { repo })}`, countdown === null ? "" : ` · ${t("Retrying in {seconds} s · press Retry to try again now", { seconds: countdown })}` ) : null, - state !== "stale" && missing.length > 0 ? (0, import_react14.createElement)("p", { className: "bt-del-partial", role: "status" }, missing.map((entry) => missingText(entry)).join(" · ")) : null, - (0, import_react14.createElement)(DeliveryHeader, { view: payload, repo, onRetry, onProbe }), - (0, import_react14.createElement)( + state !== "stale" && missing.length > 0 ? (0, import_react15.createElement)("p", { className: "bt-del-partial", role: "status" }, missing.map((entry) => missingText(entry)).join(" · ")) : null, + (0, import_react15.createElement)(DeliveryHeader, { view: payload, repo, onRetry, onProbe }), + (0, import_react15.createElement)( "div", { className: "bt-del-columns" }, - DELIVERY_COLUMNS.map((column) => (0, import_react14.createElement)( + DELIVERY_COLUMNS.map((column) => (0, import_react15.createElement)( "section", { key: column.id, className: "bt-del-column", "data-column": column.id }, - (0, import_react14.createElement)( + (0, import_react15.createElement)( "h3", { className: "bt-board-column-head" }, - (0, import_react14.createElement)("span", null, t(column.label)), - (0, import_react14.createElement)("span", { className: "bt-board-count" }, String((columns[column.id] ?? []).length)) + (0, import_react15.createElement)("span", null, t(column.label)), + (0, import_react15.createElement)("span", { className: "bt-board-count" }, String((columns[column.id] ?? []).length)) ), - (columns[column.id] ?? []).length === 0 ? (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("Nothing here.")) : (columns[column.id] ?? []).map((card) => (0, import_react14.createElement)(DeliveryCard, { key: `${card.kind}-${card.number}`, card })) + (columns[column.id] ?? []).length === 0 ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("Nothing here.")) : (columns[column.id] ?? []).map((card) => (0, import_react15.createElement)(DeliveryCard, { key: `${card.kind}-${card.number}`, card })) )) ) ); } function DeliveryView({ actions }) { const [view, retry] = useDeliveryBoard(actions); - return (0, import_react14.createElement)(DeliveryBoard, { + return (0, import_react15.createElement)(DeliveryBoard, { view, repo: view?.repo ?? null, onRetry: actions?.deliveryBoard ? retry : null, @@ -8405,25 +8603,25 @@ function DeliveryView({ actions }) { }); } function Label({ name }) { - return (0, import_react14.createElement)("span", { className: "bt-del-label", "data-label": name }, name); + return (0, import_react15.createElement)("span", { className: "bt-del-label", "data-label": name }, name); } function DeliveryCard({ card }) { const badge = badgeLabel(card?.badge); - return (0, import_react14.createElement)( + return (0, import_react15.createElement)( "article", { className: "bt-del-card", "data-kind": card?.kind, "data-number": card?.number, "data-badge": card?.badge ?? void 0 }, - (0, import_react14.createElement)( + (0, import_react15.createElement)( "span", { className: "bt-del-card-top" }, - (0, import_react14.createElement)("span", { className: "bt-del-id" }, `#${card?.number ?? "?"}`), - badge ? (0, import_react14.createElement)("span", { className: "bt-del-badge", "data-badge": card?.badge }, badge) : null + (0, import_react15.createElement)("span", { className: "bt-del-id" }, `#${card?.number ?? "?"}`), + badge ? (0, import_react15.createElement)("span", { className: "bt-del-badge", "data-badge": card?.badge }, badge) : null ), - (0, import_react14.createElement)("span", { className: "bt-del-title" }, card?.title ?? ""), - card?.labels?.length ? (0, import_react14.createElement)("span", { className: "bt-del-labels" }, card.labels.map((name) => (0, import_react14.createElement)(Label, { key: name, name }))) : null, - card?.owner ? (0, import_react14.createElement)("span", { className: "bt-del-owner", "data-kind": card.ownerKind ?? "human" }, card.owner) : null, - card?.linked ? (0, import_react14.createElement)("span", { className: "bt-del-linked" }, t("linked to #{number}", { number: card.linked })) : null, - card?.sha ? (0, import_react14.createElement)("span", { className: "bt-del-sha" }, card.sha) : null, - card?.url ? (0, import_react14.createElement)("a", { className: "bt-del-sha", href: card.url, target: "_blank", rel: "noreferrer" }, t("open on GitHub")) : null + (0, import_react15.createElement)("span", { className: "bt-del-title" }, card?.title ?? ""), + card?.labels?.length ? (0, import_react15.createElement)("span", { className: "bt-del-labels" }, card.labels.map((name) => (0, import_react15.createElement)(Label, { key: name, name }))) : null, + card?.owner ? (0, import_react15.createElement)("span", { className: "bt-del-owner", "data-kind": card.ownerKind ?? "human" }, card.owner) : null, + card?.linked ? (0, import_react15.createElement)("span", { className: "bt-del-linked" }, t("linked to #{number}", { number: card.linked })) : null, + card?.sha ? (0, import_react15.createElement)("span", { className: "bt-del-sha" }, card.sha) : null, + card?.url ? (0, import_react15.createElement)("a", { className: "bt-del-sha", href: card.url, target: "_blank", rel: "noreferrer" }, t("open on GitHub")) : null ); } var DELIVERY_CSS = ` @@ -8468,18 +8666,18 @@ var DELIVERY_CSS = ` .bt-del-columns{flex-direction:column} .bt-del-column{min-width:0} } -`; +` + DELIVERY_CHART_CSS; // src/client/kanban.js -var import_react15 = require("react"); +var import_react16 = require("react"); var POLL_MS4 = 15e3; function BoardIcon() { - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "svg", { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, - (0, import_react15.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), - (0, import_react15.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), - (0, import_react15.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) + (0, import_react16.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), + (0, import_react16.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), + (0, import_react16.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) ); } function shortId(id) { @@ -8607,10 +8805,10 @@ function lanesOf(tasks, roster) { return order.map((id) => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })); } function useBoardTasks(actions, scope) { - const [own, setOwn] = (0, import_react15.useState)([]); - const [groupTasks, setGroupTasks] = (0, import_react15.useState)([]); + const [own, setOwn] = (0, import_react16.useState)([]); + const [groupTasks, setGroupTasks] = (0, import_react16.useState)([]); const key = scope?.kind === "group" ? `group:${scope.roomId}` : "own"; - (0, import_react15.useEffect)(() => { + (0, import_react16.useEffect)(() => { let stopped = false; const load = () => { const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.()); @@ -8636,7 +8834,7 @@ function useBoardTasks(actions, scope) { return { own, tasks, replace }; } function RailButton({ row, selected, meta, onSelect }) { - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "button", { type: "button", @@ -8648,8 +8846,8 @@ function RailButton({ row, selected, meta, onSelect }) { style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : void 0, onClick: () => onSelect(row.kind === "group" ? { kind: "group", roomId: row.id } : { kind: "bot", botId: row.id }) }, - (0, import_react15.createElement)("span", { className: "bt-board-rail-name" }, row.name), - meta ? (0, import_react15.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null + (0, import_react16.createElement)("span", { className: "bt-board-rail-name" }, row.name), + meta ? (0, import_react16.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null ); } function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { @@ -8659,7 +8857,7 @@ function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { const who = botName(roster, task); const question = task.waitingFor === "input" ? task.inputRequest?.question : ""; const overall = task.kind === "group"; - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "article", { className: "bt-board-card", @@ -8675,35 +8873,35 @@ function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { }, onDragEnd: () => onDrag?.(null) }, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "span", { className: "bt-board-card-top" }, - (0, import_react15.createElement)("span", { className: "bt-board-id" }, shortId(task.id)), - (0, import_react15.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)), - (0, import_react15.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status)) + (0, import_react16.createElement)("span", { className: "bt-board-id" }, shortId(task.id)), + (0, import_react16.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)), + (0, import_react16.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status)) ), - (0, import_react15.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")), - overall ? (0, import_react15.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null, - partOf2 ? (0, import_react15.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null, - task.note ? (0, import_react15.createElement)("span", { className: "bt-board-note" }, task.note) : null, - question ? (0, import_react15.createElement)("span", { className: "bt-board-wait" }, question) : null, - (0, import_react15.createElement)( + (0, import_react16.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")), + overall ? (0, import_react16.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null, + partOf2 ? (0, import_react16.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null, + task.note ? (0, import_react16.createElement)("span", { className: "bt-board-note" }, task.note) : null, + question ? (0, import_react16.createElement)("span", { className: "bt-board-wait" }, question) : null, + (0, import_react16.createElement)( "span", { className: "bt-board-foot" }, - who ? (0, import_react15.createElement)("span", { className: "bt-board-who" }, who) : null, - (0, import_react15.createElement)( + who ? (0, import_react16.createElement)("span", { className: "bt-board-who" }, who) : null, + (0, import_react16.createElement)( "span", { className: "bt-board-progress" }, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "span", { className: "bt-board-progress-bar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progress, "aria-label": t("Progress") }, - (0, import_react15.createElement)("span", { style: { width: `${progress}%` } }) + (0, import_react16.createElement)("span", { style: { width: `${progress}%` } }) ), - (0, import_react15.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress })) + (0, import_react16.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress })) ) ), - movable && column === "not-started" ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "in-progress") }, t("Start")) : null, - movable && column === "in-progress" && task.status !== "blocked" ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null + movable && column === "not-started" ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "in-progress") }, t("Start")) : null, + movable && column === "in-progress" && task.status !== "blocked" ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null ); } function cardProps(task, roster, onMove, onDrag, partOf2) { @@ -8713,7 +8911,7 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) const tree = lanes ? columnTree(tasks) : null; const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "section", { className: "bt-board-column", @@ -8730,31 +8928,31 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) onDrag?.(null); } }, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "h3", { className: "bt-board-column-head" }, - (0, import_react15.createElement)("span", null, columnTitle(column)), - (0, import_react15.createElement)("span", { className: "bt-board-count" }, String(tasks.length)) + (0, import_react16.createElement)("span", null, columnTitle(column)), + (0, import_react16.createElement)("span", { className: "bt-board-count" }, String(tasks.length)) ), - dragging ? (0, import_react15.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, - tasks.length === 0 && !dragging ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, - tree ? tree.nests.map((nest) => (0, import_react15.createElement)( + dragging ? (0, import_react16.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, + tasks.length === 0 && !dragging ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, + tree ? tree.nests.map((nest) => (0, import_react16.createElement)( "div", { key: nest.task.id, className: "bt-board-nest" }, - (0, import_react15.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), - nest.children.map((child) => (0, import_react15.createElement)( + (0, import_react16.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), + nest.children.map((child) => (0, import_react16.createElement)( "div", { key: child.id, className: "bt-board-subtask", "data-parent": nest.task.id }, - (0, import_react15.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag)) + (0, import_react16.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag)) )) )) : null, - linked.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })), - lanes ? lanesOf(plain, roster).map((lane) => (0, import_react15.createElement)( + linked.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })), + lanes ? lanesOf(plain, roster).map((lane) => (0, import_react16.createElement)( "div", { key: lane.id, className: "bt-board-lane" }, - (0, import_react15.createElement)("span", { className: "bt-board-lane-name" }, lane.name), - lane.tasks.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) - )) : plain.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) + (0, import_react16.createElement)("span", { className: "bt-board-lane-name" }, lane.name), + lane.tasks.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) + )) : plain.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) ); } function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { @@ -8762,16 +8960,16 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro const lanes = scope?.kind === "group"; const selectedBot = scope?.kind === "bot" ? scope.botId : ""; const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "div", { className: "bt-board-body" }, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "aside", { className: "bt-board-rail", "aria-label": t("Task board") }, - (0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")), - rail.bots.length === 0 ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => { + (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")), + rail.bots.length === 0 ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => { const count = counts2?.[row.id] ?? { done: 0, total: 0 }; - return (0, import_react15.createElement)(RailButton, { + return (0, import_react16.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedBot, @@ -8779,27 +8977,27 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro meta: count.total > 0 ? t("{done}/{total}", { done: count.done, total: count.total }) : "" }); }), - rail.groups.length > 0 ? (0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, - rail.groups.map((row) => (0, import_react15.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) + rail.groups.length > 0 ? (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, + rail.groups.map((row) => (0, import_react16.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) ), - (0, import_react15.createElement)( + (0, import_react16.createElement)( "div", { className: "bt-board-main" }, - error ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, - lanes ? (0, import_react15.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, - scope?.kind === "bot" && onCreate ? (0, import_react15.createElement)( + error ? (0, import_react16.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, + lanes ? (0, import_react16.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, + scope?.kind === "bot" && onCreate ? (0, import_react16.createElement)( "form", { className: "bt-board-add", onSubmit: (event) => { event.preventDefault(); onCreate(draft); } }, - (0, import_react15.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }), - (0, import_react15.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task")) + (0, import_react16.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }), + (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task")) ) : null, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "div", { className: "bt-board-columns" }, - ["not-started", "in-progress", "done"].map((column) => (0, import_react15.createElement)(BoardColumn, { + ["not-started", "in-progress", "done"].map((column) => (0, import_react16.createElement)(BoardColumn, { key: column, column, tasks: columns[column], @@ -8816,7 +9014,7 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro function KanbanRow({ wide, useUi, actions }) { const open = useUi ? useUi((value) => Boolean(value.board)) : false; const label = t("Tasks"); - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "button", { type: "button", @@ -8827,8 +9025,8 @@ function KanbanRow({ wide, useUi, actions }) { "aria-expanded": open, onClick: () => actions.openBoard() }, - (0, import_react15.createElement)("span", { className: "bt-board-mark" }, (0, import_react15.createElement)(BoardIcon)), - wide ? (0, import_react15.createElement)("span", { className: "bt-board-label" }, label) : null + (0, import_react16.createElement)("span", { className: "bt-board-mark" }, (0, import_react16.createElement)(BoardIcon)), + wide ? (0, import_react16.createElement)("span", { className: "bt-board-label" }, label) : null ); } function scopeName(roster, scope) { @@ -8837,14 +9035,14 @@ function scopeName(roster, scope) { return ""; } function KanbanDialog({ roster, actions, leaving = false }) { - const [view, setView] = (0, import_react15.useState)("tasks"); + const [view, setView] = (0, import_react16.useState)("tasks"); const rail = boardRail(roster); - const [scope, setScope] = (0, import_react15.useState)(() => rail.bots[0] ? { kind: "bot", botId: rail.bots[0].id } : rail.groups[0] ? { kind: "group", roomId: rail.groups[0].id } : null); - const [dragging, setDragging] = (0, import_react15.useState)(null); - const [error, setError] = (0, import_react15.useState)(""); - const [draft, setDraft] = (0, import_react15.useState)(""); + const [scope, setScope] = (0, import_react16.useState)(() => rail.bots[0] ? { kind: "bot", botId: rail.bots[0].id } : rail.groups[0] ? { kind: "group", roomId: rail.groups[0].id } : null); + const [dragging, setDragging] = (0, import_react16.useState)(null); + const [error, setError] = (0, import_react16.useState)(""); + const [draft, setDraft] = (0, import_react16.useState)(""); const { own, tasks, replace } = useBoardTasks(actions, scope); - const close = (0, import_react15.useCallback)(() => actions.closeBoard(), []); + const close = (0, import_react16.useCallback)(() => actions.closeBoard(), []); useEscape(close); const counts2 = {}; for (const task of own) { @@ -8876,28 +9074,28 @@ function KanbanDialog({ roster, actions, leaving = false }) { } }; const name = scopeName(roster, scope); - return (0, import_react15.createElement)( + return (0, import_react16.createElement)( "div", { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, - (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), - (0, import_react15.createElement)( + (0, import_react16.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react16.createElement)( "div", { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, - (0, import_react15.createElement)( + (0, import_react16.createElement)( "div", { className: "bt-mem-head" }, - (0, import_react15.createElement)("h2", { id: "bt-board-heading" }, view === "delivery" ? t("Delivery") : t("Tasks")), - (0, import_react15.createElement)( + (0, import_react16.createElement)("h2", { id: "bt-board-heading" }, view === "delivery" ? t("Delivery") : t("Tasks")), + (0, import_react16.createElement)( "div", { className: "bt-board-views", role: "tablist", "aria-label": t("Board view") }, - (0, import_react15.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "tasks", "aria-selected": view === "tasks" ? "true" : "false", onClick: () => setView("tasks") }, t("Tasks")), - (0, import_react15.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "delivery", "aria-selected": view === "delivery" ? "true" : "false", onClick: () => setView("delivery") }, t("Delivery")) + (0, import_react16.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "tasks", "aria-selected": view === "tasks" ? "true" : "false", onClick: () => setView("tasks") }, t("Tasks")), + (0, import_react16.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "delivery", "aria-selected": view === "delivery" ? "true" : "false", onClick: () => setView("delivery") }, t("Delivery")) ), - view === "tasks" ? (0, import_react15.createElement)("span", { className: "bt-mem-status" }, name) : null, - (0, import_react15.createElement)("span", { className: "bt-mem-head-gap" }), - (0, import_react15.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react15.createElement)(CloseIcon)) + view === "tasks" ? (0, import_react16.createElement)("span", { className: "bt-mem-status" }, name) : null, + (0, import_react16.createElement)("span", { className: "bt-mem-head-gap" }), + (0, import_react16.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react16.createElement)(CloseIcon)) ), - view === "delivery" ? (0, import_react15.createElement)(DeliveryView, { actions }) : (0, import_react15.createElement)(BoardView, { + view === "delivery" ? (0, import_react16.createElement)(DeliveryView, { actions }) : (0, import_react16.createElement)(BoardView, { roster, tasks, rail, @@ -8982,22 +9180,22 @@ var KANBAN_CSS = ` ` + DELIVERY_CSS; // src/client/sidebar.js -var import_react16 = require("react"); +var import_react17 = require("react"); var import_react_dom3 = require("react-dom"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); function useMarkedUnread(id, active, marked, actions) { - (0, import_react16.useEffect)(() => { + (0, import_react17.useEffect)(() => { if (active && marked) actions.clearUnread(id); }, [active, marked]); } -var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.createElement)(PinMarkIcon)); +var PinMark = () => (0, import_react17.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react17.createElement)(PinMarkIcon)); var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { const [id, tool] = key.split("|"); return { id, tool: tool || void 0 }; }); -var workLine = (lead, tool, more) => (0, import_react16.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); +var workLine = (lead, tool, more) => (0, import_react17.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id; const part = currentPart(bot); @@ -9028,7 +9226,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio const secondLine = awayLine || preview || (remote ? bot.remote?.gone ? t("no longer shared") : t("Friend's Bot") : ""); const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); if (tile && wide) { - return (0, import_react16.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react16.createElement)( + return (0, import_react17.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react17.createElement)( "button", { type: "button", @@ -9040,15 +9238,15 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio onClick: () => actions.openSession(id), onContextMenu }, - (0, import_react16.createElement)(BotAvatar, { bot, size: 60, state, main: true }), - (0, import_react16.createElement)("span", { className: "bt-tile-name" }, shown), + (0, import_react17.createElement)(BotAvatar, { bot, size: 60, state, main: true }), + (0, import_react17.createElement)("span", { className: "bt-tile-name" }, shown), // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. - unread && !waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, - bot.role ? (0, import_react16.createElement)("span", { className: "bt-chip" }, bot.role) : null + unread && !waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, + bot.role ? (0, import_react17.createElement)("span", { className: "bt-chip" }, bot.role) : null )); } - const unreadMark = wide && waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; - const row = (0, import_react16.createElement)( + const unreadMark = wide && waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; + const row = (0, import_react17.createElement)( "button", { type: "button", @@ -9061,24 +9259,24 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio onClick: () => actions.openSession(id), onContextMenu }, - (0, import_react16.createElement)(BotAvatar, { bot, size: 36, state, main }), - pinned && !wide ? (0, import_react16.createElement)(PinMark) : null, - wide ? (0, import_react16.createElement)( + (0, import_react17.createElement)(BotAvatar, { bot, size: 36, state, main }), + pinned && !wide ? (0, import_react17.createElement)(PinMark) : null, + wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "span", { className: "bt-row-name" }, - (0, import_react16.createElement)("span", { className: "bt-row-title" }, shown), - bot.remote ? (0, import_react16.createElement)(RemoteMark, { remote: bot.remote }) : null, - pinned ? (0, import_react16.createElement)(PinMark) : null + (0, import_react17.createElement)("span", { className: "bt-row-title" }, shown), + bot.remote ? (0, import_react17.createElement)(RemoteMark, { remote: bot.remote }) : null, + pinned ? (0, import_react17.createElement)(PinMark) : null ), - secondLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, secondLine) : null + secondLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ) : null, unreadMark ); const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); - const settings = wide ? (0, import_react16.createElement)("button", { + const settings = wide ? (0, import_react17.createElement)("button", { type: "button", className: "bt-row-settings", "aria-label": settingsLabel, @@ -9089,8 +9287,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio keepSidebarStill(event.currentTarget); actions.openSettings("bots", id); } - }, (0, import_react16.createElement)(GearIcon)) : null; - if (settings) return (0, import_react16.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); + }, (0, import_react17.createElement)(GearIcon)) : null; + if (settings) return (0, import_react17.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); return row; } function keepSidebarStill(node) { @@ -9103,14 +9301,14 @@ function keepSidebarStill(node) { if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); } function RemoteMark({ remote }) { - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "span", { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "svg", { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, - (0, import_react16.createElement)("circle", { cx: 12, cy: 12, r: 9 }), - (0, import_react16.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + (0, import_react17.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react17.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) ) ); } @@ -9122,15 +9320,15 @@ function isPastFaceRoom(room2) { return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); } function FaceMark() { - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "span", { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "svg", { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, - (0, import_react16.createElement)("circle", { cx: 9, cy: 8, r: 3 }), - (0, import_react16.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), - (0, import_react16.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) + (0, import_react17.createElement)("circle", { cx: 9, cy: 8, r: 3 }), + (0, import_react17.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), + (0, import_react17.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) ) ); } @@ -9155,7 +9353,7 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, const alert = question || waiting; const face = room2.origin === "face-to-face"; const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", "); - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "button", { type: "button", @@ -9172,20 +9370,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, - (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), - pinned && !wide ? (0, import_react16.createElement)(PinMark) : null, - wide ? (0, import_react16.createElement)( + (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), + pinned && !wide ? (0, import_react17.createElement)(PinMark) : null, + wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react16.createElement)("span", { className: "bt-row-name" }, (0, import_react16.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react16.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react16.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react16.createElement)(PinMark) : null), - workingLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null + (0, import_react17.createElement)("span", { className: "bt-row-name" }, (0, import_react17.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react17.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react17.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react17.createElement)(PinMark) : null), + workingLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null ) : null, - wide && alert && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + wide && alert && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ); } function AgentSessionRow({ session, agent, actions, renaming, index }) { - const input = (0, import_react16.useRef)(null); - (0, import_react16.useLayoutEffect)(() => { + const input = (0, import_react17.useRef)(null); + (0, import_react17.useLayoutEffect)(() => { if (!renaming) return; input.current?.focus(); input.current?.select(); @@ -9206,10 +9404,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { } })(); }; - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "div", { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, - (0, import_react16.createElement)("input", { + (0, import_react17.createElement)("input", { ref: input, defaultValue: session.title, "aria-label": t("Rename"), @@ -9222,7 +9420,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { }) ); } - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "button", { type: "button", @@ -9236,8 +9434,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, - (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, session.title), - (0, import_react16.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") + (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, session.title), + (0, import_react17.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { @@ -9272,10 +9470,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "div", { className: "bt-agent" }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "button", { type: "button", @@ -9290,30 +9488,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, - (0, import_react16.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react16.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), - wide ? (0, import_react16.createElement)( + (0, import_react17.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react17.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), + wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react16.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), - preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null + (0, import_react17.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), + preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null ) : null, - wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ), - (0, import_react16.createElement)( + (0, import_react17.createElement)( "div", { className: "bt-agent-sub", "data-open": open || void 0 }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "div", { className: "bt-agent-sub-in" }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "button", { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); } }, - (0, import_react16.createElement)(PlusIcon), - (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) + (0, import_react17.createElement)(PlusIcon), + (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) ), - sessions.map((session, index) => (0, import_react16.createElement)(AgentSessionRow, { + sessions.map((session, index) => (0, import_react17.createElement)(AgentSessionRow, { key: session.id, session, agent, @@ -9327,8 +9525,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { - const tops = (0, import_react16.useRef)({ layout, map: /* @__PURE__ */ new Map() }); - (0, import_react16.useLayoutEffect)(() => { + const tops = (0, import_react17.useRef)({ layout, map: /* @__PURE__ */ new Map() }); + (0, import_react17.useLayoutEffect)(() => { const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); @@ -9345,7 +9543,7 @@ function useListGlide(listRef, layout, key) { tops.current = { layout, map: next }; }, [layout, key]); } -var NodeActions = class extends import_react16.Component { +var NodeActions = class extends import_react17.Component { constructor(props) { super(props); this.state = { failed: false }; @@ -9357,32 +9555,32 @@ var NodeActions = class extends import_react16.Component { console.error("flybot: nodeActions failed", error); } render() { - return this.state.failed ? null : (0, import_react16.createElement)(this.props.component, { bot: this.props.bot }); + return this.state.failed ? null : (0, import_react17.createElement)(this.props.component, { bot: this.props.bot }); } }; function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { const { bot, depth, children } = node; const closed = collapsed[bot.id] === true; - return (0, import_react16.createElement)( - import_react16.Fragment, + return (0, import_react17.createElement)( + import_react17.Fragment, null, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "div", { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, - children.length ? (0, import_react16.createElement)( + children.length ? (0, import_react17.createElement)( "button", { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, - (0, import_react16.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") - ) : (0, import_react16.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), - (0, import_react16.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), - typeof actions === "function" ? (0, import_react16.createElement)(NodeActions, { component: actions, bot }) : null + (0, import_react17.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") + ) : (0, import_react17.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), + (0, import_react17.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), + typeof actions === "function" ? (0, import_react17.createElement)(NodeActions, { component: actions, bot }) : null ), - closed ? null : children.map((child) => (0, import_react16.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) + closed ? null : children.map((child) => (0, import_react17.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) ); } function TeamSidebar(props) { const { wide, useRoster, useUi, useSessions, actions } = props; - const listRef = (0, import_react16.useRef)(null); + const listRef = (0, import_react17.useRef)(null); const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); @@ -9391,7 +9589,7 @@ function TeamSidebar(props) { for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); - const order = (0, import_react16.useMemo)(() => { + const order = (0, import_react17.useMemo)(() => { const stamps = JSON.parse(updated); const entries2 = [ // Own Bots, friends' Bots, groups and face rooms share one list. A remote chat @@ -9406,7 +9604,7 @@ function TeamSidebar(props) { }, [roster, updated]); const main = roster.byId[roster.mainBotId]; const structure = useSidebarStructure(); - const [collapsed, setCollapsed] = (0, import_react16.useState)({}); + const [collapsed, setCollapsed] = (0, import_react17.useState)({}); const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); const inTree = (entry) => entry.kind === "bot" && !entry.item.remote; const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false; @@ -9420,42 +9618,42 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "div", { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, - wide ? (0, import_react16.createElement)( + wide ? (0, import_react17.createElement)( "div", { className: "bt-side-top" }, - (0, import_react16.createElement)("span", { className: "bt-brand" }, (0, import_react16.createElement)(BrandMark, { size: 22 }), (0, import_react16.createElement)(BrandName)), - (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), - (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react16.createElement)(SearchIcon)), - (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon)) - ) : (0, import_react16.createElement)( - import_react16.Fragment, + (0, import_react17.createElement)("span", { className: "bt-brand" }, (0, import_react17.createElement)(BrandMark, { size: 22 }), (0, import_react17.createElement)(BrandName)), + (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), + (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react17.createElement)(SearchIcon)), + (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon)) + ) : (0, import_react17.createElement)( + import_react17.Fragment, null, - (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), - (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon)) + (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), + (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon)) ), - wide && roster.readOnly ? (0, import_react16.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, - (0, import_react16.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), - (0, import_react16.createElement)(KanbanRow, { wide, useUi, actions }), - (0, import_react16.createElement)( + wide && roster.readOnly ? (0, import_react17.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + (0, import_react17.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), + (0, import_react17.createElement)(KanbanRow, { wide, useUi, actions }), + (0, import_react17.createElement)( "div", { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, - tree ? (0, import_react16.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react16.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react16.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react16.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), - order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react16.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react16.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react16.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), - wide ? (0, import_react16.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react16.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null + tree ? (0, import_react17.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react17.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react17.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react17.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), + order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react17.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react17.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react17.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), + wide ? (0, import_react17.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react17.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null ) ); } function UpdateButton({ useUpdate, actions }) { const update = useUpdate((value) => value); - const [open, setOpen] = (0, import_react16.useState)(false); - const [box, setBox] = (0, import_react16.useState)(null); - const [error, setError] = (0, import_react16.useState)(""); - const [dshError, setDshError] = (0, import_react16.useState)(""); - const button = (0, import_react16.useRef)(null); - (0, import_react16.useEffect)(() => { + const [open, setOpen] = (0, import_react17.useState)(false); + const [box, setBox] = (0, import_react17.useState)(null); + const [error, setError] = (0, import_react17.useState)(""); + const [dshError, setDshError] = (0, import_react17.useState)(""); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; @@ -9498,10 +9696,10 @@ function UpdateButton({ useUpdate, actions }) { const message = error || (phase === "failed" ? update.error : ""); const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); - return (0, import_react16.createElement)( - import_react16.Fragment, + return (0, import_react17.createElement)( + import_react17.Fragment, null, - (0, import_react16.createElement)("button", { + (0, import_react17.createElement)("button", { ref: button, type: "button", className: "bt-round bt-update", @@ -9511,21 +9709,21 @@ function UpdateButton({ useUpdate, actions }) { "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle - }, (0, import_react16.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react16.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), - open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)( + }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), + open && box ? (0, import_react_dom3.createPortal)((0, import_react17.createElement)( "div", { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box }, - update?.available || phase !== "idle" ? (0, import_react16.createElement)( - import_react16.Fragment, + update?.available || phase !== "idle" ? (0, import_react17.createElement)( + import_react17.Fragment, null, - (0, import_react16.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), - (0, import_react16.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), - message ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, - (0, import_react16.createElement)( + (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), + message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react17.createElement)( "div", { className: "bt-update-actions" }, - update.notesUrl ? (0, import_react16.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, - phase === "installed" ? null : (0, import_react16.createElement)("button", { + update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", disabled: phase === "installing", @@ -9533,17 +9731,17 @@ function UpdateButton({ useUpdate, actions }) { }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, - dshRow ? (0, import_react16.createElement)( + dshRow ? (0, import_react17.createElement)( "div", { className: "bt-update-dsh" }, - (0, import_react16.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), - (0, import_react16.createElement)("div", { className: "bt-update-text" }, dshText), - dshMessage ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, - (0, import_react16.createElement)( + (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText), + dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, + (0, import_react17.createElement)( "div", { className: "bt-update-actions" }, - dsh.kind === "app" ? dsh.available ? (0, import_react16.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { - }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react16.createElement)("button", { + dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { + }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", disabled: dshPhase === "installing", @@ -9556,23 +9754,23 @@ function UpdateButton({ useUpdate, actions }) { } function ConnectApps({ wide, actions }) { if (!wide) return null; - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "span", { className: "bt-connect-wrap" }, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "button", { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, - (0, import_react16.createElement)(PlugIcon), + (0, import_react17.createElement)(PlugIcon), t("Connect apps") ) ); } function YouAvatar() { - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "span", { className: "bt-you-wrap" }, - (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), - (0, import_react16.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ); } var openShellSettings = null; @@ -9604,7 +9802,7 @@ function openHarnessOnboarding(id) { var MODELS_STEP_ID = "flybot-models"; var MODELS_STEP_ORDER = 1e3; function ModelsSettingsStep({ explicit, complete, openSection }) { - (0, import_react16.useLayoutEffect)(() => { + (0, import_react17.useLayoutEffect)(() => { if (explicit) openSection("models"); complete(); }, []); @@ -9642,32 +9840,32 @@ function pressSettingsShortcut() { })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { - const [open, setOpen] = (0, import_react16.useState)(false); - (0, import_react16.useLayoutEffect)(() => { + const [open, setOpen] = (0, import_react17.useState)(false); + (0, import_react17.useLayoutEffect)(() => { if (!seat) return void 0; actions.seatAccount(true); return () => actions.seatAccount(false); }, [seat]); - const [box, setBox] = (0, import_react16.useState)(null); - const button = (0, import_react16.useRef)(null); - (0, import_react16.useEffect)(() => { + const [box, setBox] = (0, import_react17.useState)(null); + const button = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { if (settingsOpen) setOpen(false); }, [settingsOpen]); - (0, import_react16.useEffect)(() => { + (0, import_react17.useEffect)(() => { if (!openSettings) return void 0; openShellSettings = openSettings; return () => { if (openShellSettings === openSettings) openShellSettings = null; }; }, [openSettings]); - (0, import_react16.useEffect)(() => { + (0, import_react17.useEffect)(() => { if (!openOnboarding) return void 0; openShellOnboarding = openOnboarding; return () => { if (openShellOnboarding === openOnboarding) openShellOnboarding = null; }; }, [openOnboarding]); - (0, import_react16.useEffect)(() => { + (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; @@ -9704,23 +9902,23 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] ] ]; - return (0, import_react16.createElement)( - import_react16.Fragment, + return (0, import_react17.createElement)( + import_react17.Fragment, null, - (0, import_react16.createElement)( + (0, import_react17.createElement)( "button", { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, - (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ), - open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)( + open && box ? (0, import_react_dom3.createPortal)((0, import_react17.createElement)( "div", { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, - (0, import_react16.createElement)(FlyBotAccountHeader, { actions }), - groups.map((group, index) => (0, import_react16.createElement)( - import_react16.Fragment, + (0, import_react17.createElement)(FlyBotAccountHeader, { actions }), + groups.map((group, index) => (0, import_react17.createElement)( + import_react17.Fragment, { key: index }, - index > 0 ? (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, hint]) => (0, import_react16.createElement)("button", { + index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", { key: label, type: "button", role: "menuitem", @@ -9728,7 +9926,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } - }, (0, import_react16.createElement)(Icon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) )) ), document.body) : null ); @@ -9738,8 +9936,8 @@ function useLandOnMainBot(roster, useSessions, actions) { const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); - const lastBot = (0, import_react16.useRef)(null); - (0, import_react16.useEffect)(() => { + const lastBot = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; @@ -9760,16 +9958,16 @@ function useLandOnMainBot(roster, useSessions, actions) { function FooterAccount(props) { const seated = props.useUi((value) => value.accountSeated === true); if (seated) return null; - return (0, import_react16.createElement)("span", { className: "bt-foot-account" }, (0, import_react16.createElement)(AccountLauncher, { ...props, seat: false })); + return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false })); } function BotReturn({ wide, useSurface, actions }) { const mode = useSurface((value) => value.mode); if (mode !== "agent") return null; - return (0, import_react16.createElement)( + return (0, import_react17.createElement)( "button", { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, - (0, import_react16.createElement)(FlyBotMark, { size: 20 }), - wide ? (0, import_react16.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null + (0, import_react17.createElement)(FlyBotMark, { size: 20 }), + wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null ); } var RETURN_CSS = ` @@ -9783,10 +9981,10 @@ var RETURN_CSS = ` .bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} `; function BrandMark({ size = 24 }) { - return (0, import_react16.createElement)("span", { className: "bt-brand-mark" }, (0, import_react16.createElement)(FlyBotMark, { size })); + return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size })); } function BrandName() { - return (0, import_react16.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); + return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} @@ -9860,15 +10058,15 @@ var SIDEBAR_CSS = ` `; // src/client/header.js -var import_react17 = require("react"); +var import_react18 = require("react"); var import_react_dom4 = require("react-dom"); -var GoArrow = () => (0, import_react17.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react17.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); +var GoArrow = () => (0, import_react18.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react18.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); var MEMORY_NOTICE_MS = 3200; var noNews = (select) => select({}); function useMemoryNotice(ids, useMemoryNews) { const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); - const [shown, setShown] = (0, import_react17.useState)(false); - (0, import_react17.useEffect)(() => { + const [shown, setShown] = (0, import_react18.useState)(false); + (0, import_react18.useEffect)(() => { const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; @@ -9878,16 +10076,16 @@ function useMemoryNotice(ids, useMemoryNews) { return shown; } function MemoryNotice({ shown }) { - const inner = (0, import_react17.useRef)(null); - const [width, setWidth] = (0, import_react17.useState)(0); + const inner = (0, import_react18.useRef)(null); + const [width, setWidth] = (0, import_react18.useState)(0); const label = t("Memory updated"); - (0, import_react17.useLayoutEffect)(() => { + (0, import_react18.useLayoutEffect)(() => { if (shown) setWidth(inner.current?.scrollWidth ?? 0); }, [label, shown]); - return (0, import_react17.createElement)( + return (0, import_react18.createElement)( "span", { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, - (0, import_react17.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react17.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react17.createElement)(MemoryIcon)), label) + (0, import_react18.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react18.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react18.createElement)(MemoryIcon)), label) ); } function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { @@ -9903,7 +10101,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); - (0, import_react17.useEffect)(() => { + (0, import_react18.useEffect)(() => { if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); @@ -9922,10 +10120,10 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe const notice = useMemoryNotice(memoryIds, useNews); const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); const memoryNotice = notice && changed2 !== null && roster.memory !== false; - return (0, import_react17.createElement)( + return (0, import_react18.createElement)( "div", { className: "bt-head" }, - (0, import_react17.createElement)( + (0, import_react18.createElement)( "button", { type: "button", @@ -9935,19 +10133,19 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed2]?.name ?? full2 }) : t("View conversation details"), onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId) }, - bot ? (0, import_react17.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, - (0, import_react17.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_react17.createElement)(MemoryNotice, { shown: memoryNotice }), - (0, import_react17.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react17.createElement)(GoArrow)) + 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_react17.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), - (0, import_react17.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), - (0, import_react17.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 }) ); } function useTranscriptView() { - const [view, setView] = (0, import_react17.useState)(null); - (0, import_react17.useEffect)(() => { + const [view, setView] = (0, import_react18.useState)(null); + (0, import_react18.useEffect)(() => { let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); @@ -10012,22 +10210,22 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; - const [release, setRelease] = (0, import_react17.useState)({ base: null, dismissed: false }); - const [above, setAbove] = (0, import_react17.useState)({ seen: false, dismissed: false }); - (0, import_react17.useEffect)(() => { + const [release, setRelease] = (0, import_react18.useState)({ base: null, dismissed: false }); + const [above, setAbove] = (0, import_react18.useState)({ seen: false, dismissed: false }); + (0, import_react18.useEffect)(() => { setRelease({ base: unpinned ? replies : null, dismissed: false }); }, [unpinned]); - (0, import_react17.useEffect)(() => { + (0, import_react18.useEffect)(() => { if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); }, [view?.divider]); const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); - (0, import_react17.useEffect)(() => { + (0, import_react18.useEffect)(() => { if (above.seen && movedOn) actions?.settleVisit?.(sessionId); }, [above.seen, movedOn, sessionId]); const fresh = release.base === null ? 0 : replies - release.base; const showDown = unpinned && fresh > 0 && !release.dismissed; const showUp = view?.divider === "above" && !above.seen && !above.dismissed; - (0, import_react17.useEffect)(() => { + (0, import_react18.useEffect)(() => { const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); @@ -10035,16 +10233,16 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl }, [showDown]); if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); - const pill = (direction, count, style, onJump, onDismiss) => (0, import_react17.createElement)( + const pill = (direction, count, style, onJump, onDismiss) => (0, import_react18.createElement)( "div", { className: "bt-news", "data-direction": direction, style }, - (0, import_react17.createElement)( + (0, import_react18.createElement)( "button", { type: "button", className: "bt-news-jump", onClick: onJump }, - (0, import_react17.createElement)("span", { className: "bt-news-ico" }, (0, import_react17.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), + (0, import_react18.createElement)("span", { className: "bt-news-ico" }, (0, import_react18.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), label(count) ), - (0, import_react17.createElement)("button", { + (0, import_react18.createElement)("button", { type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), @@ -10052,7 +10250,7 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl event.stopPropagation(); onDismiss(); } - }, (0, import_react17.createElement)(CloseIcon)) + }, (0, import_react18.createElement)(CloseIcon)) ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); @@ -10062,8 +10260,8 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl }; const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); return (0, import_react_dom4.createPortal)( - (0, import_react17.createElement)( - import_react17.Fragment, + (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 @@ -10171,18 +10369,18 @@ function installHeaderEffects({ sessions, roster, actions, doc = globalThis.docu } // src/client/composer.js -var import_react18 = require("react"); +var import_react19 = require("react"); var import_react_dom5 = require("react-dom"); var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; var keepComposerFocus = (event) => event.preventDefault(); function useDictation(onText) { - const [listening, setListening] = (0, import_react18.useState)(false); - const [note, setNote] = (0, import_react18.useState)(""); - const recognition = (0, import_react18.useRef)(null); - const latest = (0, import_react18.useRef)(onText); + const [listening, setListening] = (0, import_react19.useState)(false); + const [note, setNote] = (0, import_react19.useState)(""); + const recognition = (0, import_react19.useRef)(null); + const latest = (0, import_react19.useRef)(onText); latest.current = onText; - (0, import_react18.useEffect)(() => () => recognition.current?.abort(), []); - (0, import_react18.useEffect)(() => { + (0, import_react19.useEffect)(() => () => recognition.current?.abort(), []); + (0, import_react19.useEffect)(() => { if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); @@ -10236,14 +10434,14 @@ function ComposerIdentity({ sessionId, useRoster }) { const self = roster.self; const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); if (room2?.viewerRole === "invited-owner") { - return (0, import_react18.createElement)( + return (0, import_react19.createElement)( "span", { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) ); } if (!self || !room2 || humans.length < 2) return null; - return (0, import_react18.createElement)( + return (0, import_react19.createElement)( "span", { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, t("Sending as {name}", { name: self.displayName || self.memberId }) @@ -10254,7 +10452,7 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); }); const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); - (0, import_react18.useEffect)(() => { + (0, import_react19.useEffect)(() => { if (!quote || !inputActions) return; actions.clearQuote(quote.token); const excerpt = quote.text.replace(/\s+/g, " ").trim(); @@ -10272,10 +10470,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; - return (0, import_react18.createElement)( - import_react18.Fragment, + return (0, import_react19.createElement)( + import_react19.Fragment, null, - pluginMic ? null : (0, import_react18.createElement)("button", { + pluginMic ? null : (0, import_react19.createElement)("button", { type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), @@ -10283,16 +10481,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { title: dictation.listening ? t("Stop dictation") : t("Dictate"), onMouseDown: keepComposerFocus, onClick: () => dictation.listening ? dictation.stop() : dictation.start() - }, (0, import_react18.createElement)(MicIcon)), - (0, import_react18.createElement)("button", { + }, (0, import_react19.createElement)(MicIcon)), + (0, import_react19.createElement)("button", { type: "button", className: "bt-voice", "aria-label": t("Start voice chat"), title: t("Voice chat"), onMouseDown: keepComposerFocus, onClick: () => actions.openVoice(sessionId) - }, (0, import_react18.createElement)(WaveIcon, { size: 18 })), - dictation.note ? (0, import_react18.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null + }, (0, import_react19.createElement)(WaveIcon, { size: 18 })), + dictation.note ? (0, import_react19.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ); } function menuIcons(roster) { @@ -10300,17 +10498,17 @@ function menuIcons(roster) { return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; - if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react18.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); + if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react19.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); return marks.get(id); }; } function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { const roster = useRoster((value) => value); - const [open, setOpen] = (0, import_react18.useState)(null); + const [open, setOpen] = (0, import_react19.useState)(null); const [shown, leaving] = useLinger(open, 120); - const button = (0, import_react18.useRef)(null); - const menu = (0, import_react18.useRef)(null); - (0, import_react18.useEffect)(() => { + const button = (0, import_react19.useRef)(null); + const menu = (0, import_react19.useRef)(null); + (0, import_react19.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); @@ -10351,10 +10549,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); - return (0, import_react18.createElement)( - import_react18.Fragment, + return (0, import_react19.createElement)( + import_react19.Fragment, null, - (0, import_react18.createElement)("button", { + (0, import_react19.createElement)("button", { ref: button, type: "button", className: "bt-plus", @@ -10364,8 +10562,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle - }, (0, import_react18.createElement)(PlusIcon)), - shown ? (0, import_react_dom5.createPortal)((0, import_react18.createElement)( + }, (0, import_react19.createElement)(PlusIcon)), + shown ? (0, import_react_dom5.createPortal)((0, import_react19.createElement)( "div", { ref: menu, @@ -10375,9 +10573,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, - (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react18.createElement)(PaperclipIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), - canReference ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react18.createElement)(ChatsIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, - (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react18.createElement)(ClockIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) + (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react19.createElement)(PaperclipIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), + canReference ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react19.createElement)(ChatsIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, + (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react19.createElement)(ClockIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) ), document.body) : null ); } @@ -10733,7 +10931,7 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { } // src/client/model-menu.js -var import_react19 = require("react"); +var import_react20 = require("react"); var import_react_dom6 = require("react-dom"); var MENU_EXIT_MS = 120; function groupModels(models) { @@ -10756,8 +10954,8 @@ function groupModels(models) { 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_react19.useState)(void 0); - (0, import_react19.useEffect)(() => { + const [fetched, setFetched] = (0, import_react20.useState)(void 0); + (0, import_react20.useEffect)(() => { setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; @@ -10782,12 +10980,12 @@ function effortRows(reasoning) { ]; } function EffortRows({ reasoning, current, onPick }) { - return (0, import_react19.createElement)( - import_react19.Fragment, + return (0, import_react20.createElement)( + import_react20.Fragment, null, - (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), - effortRows(reasoning).map((row) => (0, import_react19.createElement)( + (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), + effortRows(reasoning).map((row) => (0, import_react20.createElement)( "button", { key: row.effort ?? "default", @@ -10796,17 +10994,17 @@ function EffortRows({ reasoning, current, onPick }) { "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, - (0, import_react19.createElement)("span", { className: "bt-menu-label" }, row.label), - (0, import_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react19.createElement)(CheckIcon) : null) + (0, import_react20.createElement)("span", { className: "bt-menu-label" }, row.label), + (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react20.createElement)(CheckIcon) : null) )) ); } function ModelRows({ roster, current, onPick }) { - return groupModels(roster.models).map(([provider, list]) => (0, import_react19.createElement)( - import_react19.Fragment, + return groupModels(roster.models).map(([provider, list]) => (0, import_react20.createElement)( + import_react20.Fragment, { key: list[0].provider ?? provider }, - (0, import_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), - list.map((model) => (0, import_react19.createElement)( + (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), + list.map((model) => (0, import_react20.createElement)( "button", { key: model.ref, @@ -10816,20 +11014,20 @@ function ModelRows({ roster, current, onPick }) { title: model.ref, onClick: () => onPick(model.ref) }, - (0, import_react19.createElement)("span", { className: "bt-menu-label" }, model.name), - model.ref === roster.defaultModel ? (0, import_react19.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, - (0, import_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react19.createElement)(CheckIcon) : null) + (0, import_react20.createElement)("span", { className: "bt-menu-label" }, model.name), + model.ref === roster.defaultModel ? (0, import_react20.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, + (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react20.createElement)(CheckIcon) : null) )) )); } -var Caret = () => (0, import_react19.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react19.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); +var Caret = () => (0, import_react20.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react20.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); function useAnchoredMenu() { - const [place, setPlace] = (0, import_react19.useState)(null); + const [place, setPlace] = (0, import_react20.useState)(null); const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); - const button = (0, import_react19.useRef)(null); - const menu = (0, import_react19.useRef)(null); + const button = (0, import_react20.useRef)(null); + const menu = (0, import_react20.useRef)(null); const close = () => setPlace(null); - (0, import_react19.useEffect)(() => { + (0, import_react20.useEffect)(() => { if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; @@ -10892,10 +11090,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; const models = roster.models ?? []; - return (0, import_react19.createElement)( - import_react19.Fragment, + return (0, import_react20.createElement)( + import_react20.Fragment, null, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "button", { ref: button, @@ -10908,38 +11106,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, onClick: toggle }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "span", { className: "bt-model-copy" }, - (0, import_react19.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), - shown ? (0, import_react19.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null + (0, import_react20.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), + shown ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null ), - (0, import_react19.createElement)(Caret) + (0, import_react20.createElement)(Caret) ), - shownPlace ? (0, import_react_dom6.createPortal)((0, import_react19.createElement)( + shownPlace ? (0, import_react_dom6.createPortal)((0, import_react20.createElement)( "div", { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, - models.length > 0 ? (0, import_react19.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react19.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), - effortRows(reasoning).length > 0 ? (0, import_react19.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, - (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => { + models.length > 0 ? (0, import_react20.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), + effortRows(reasoning).length > 0 ? (0, import_react20.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, + (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => { close(); openHarnessModels(); - } }, (0, import_react19.createElement)(SlidersIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) + } }, (0, import_react20.createElement)(SlidersIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) ), document.body) : null ); } function ModelChoiceDock({ sessionId, useRoster, actions }) { const roster = useRoster((value) => value); const bot = roster.byId[sessionId]; - const [busy, setBusy] = (0, import_react19.useState)(""); - const [error, setError] = (0, import_react19.useState)(""); + const [busy, setBusy] = (0, import_react20.useState)(""); + const [error, setError] = (0, import_react20.useState)(""); const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; - (0, import_react19.useEffect)(() => { + (0, import_react20.useEffect)(() => { if (shown) void actions.refreshModels?.(); }, [shown, sessionId]); if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; - if (bot.remote) return bot.remote.gone ? (0, import_react19.createElement)(RemoteGoneDock, { bot }) : null; + if (bot.remote) return bot.remote.gone ? (0, import_react20.createElement)(RemoteGoneDock, { bot }) : null; const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; @@ -10952,12 +11150,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { }).finally(() => setBusy("")); }; const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); - const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react19.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react19.createElement)(KeyHelpLink, { key: "key-help" })] : [ - ...groupModels(models).map(([provider, list]) => (0, import_react19.createElement)( + const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" })] : [ + ...groupModels(models).map(([provider, list]) => (0, import_react20.createElement)( "div", { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, - (0, import_react19.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), - (0, import_react19.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react19.createElement)("button", { + (0, import_react20.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), + (0, import_react20.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react20.createElement)("button", { key: model.ref, type: "button", className: "bt-model-chip", @@ -10966,46 +11164,46 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { "aria-label": `${model.name} · ${provider}`, title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) - }, model.name, busy === model.ref ? (0, import_react19.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) + }, model.name, busy === model.ref ? (0, import_react20.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) )), - (0, import_react19.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react19.createElement)(SlidersIcon), t("Manage models")) + (0, import_react20.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react20.createElement)(SlidersIcon), t("Manage models")) ]; - return (0, import_react19.createElement)( + return (0, import_react20.createElement)( "div", { className: "bt-model-dock" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-astack", "data-lead": "" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-msg" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-msg-line" }, - (0, import_react19.createElement)(BotMark, { bot, size: 22 }), - (0, import_react19.createElement)("div", { className: "bt-bubble" }, text) + (0, import_react20.createElement)(BotMark, { bot, size: 22 }), + (0, import_react20.createElement)("div", { className: "bt-bubble" }, text) ) ) ), - (0, import_react19.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), - error ? (0, import_react19.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null + (0, import_react20.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), + error ? (0, import_react20.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null ); } function RemoteGoneDock({ bot }) { - return (0, import_react19.createElement)( + return (0, import_react20.createElement)( "div", { className: "bt-model-dock", "data-flybot": "remote-gone" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-astack", "data-lead": "" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-msg" }, - (0, import_react19.createElement)( + (0, import_react20.createElement)( "div", { className: "bt-msg-line" }, - (0, import_react19.createElement)(BotMark, { bot, size: 22 }), - (0, import_react19.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) + (0, import_react20.createElement)(BotMark, { bot, size: 22 }), + (0, import_react20.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) ) ) ) @@ -11020,7 +11218,7 @@ function setupChoices(actions) { } var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; function KeyHelpLink() { - return (0, import_react19.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); + return (0, import_react20.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); } function missingModelKey(message) { const text = String(message ?? ""); @@ -11044,22 +11242,22 @@ function TurnErrorCell({ node, t: chatT, actions }) { const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; const deepseek = missing ? /^deepseek/i.test(missing.route) : false; - return (0, import_react19.createElement)( + return (0, import_react20.createElement)( "div", { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, - (0, import_react19.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), - (0, import_react19.createElement)( + (0, import_react20.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), + (0, import_react20.createElement)( "div", { className: "bt-turn-error-copy" }, - (0, import_react19.createElement)("span", { className: "bt-turn-error-title" }, title), - (0, import_react19.createElement)("span", { className: "bt-turn-error-message" }, message), - code ? (0, import_react19.createElement)("code", { className: "bt-turn-error-code" }, code) : null + (0, import_react20.createElement)("span", { className: "bt-turn-error-title" }, title), + (0, import_react20.createElement)("span", { className: "bt-turn-error-message" }, message), + code ? (0, import_react20.createElement)("code", { className: "bt-turn-error-code" }, code) : null ), - choices.length > 0 ? (0, import_react19.createElement)( + choices.length > 0 ? (0, import_react20.createElement)( "div", { className: "bt-turn-error-actions" }, - choices.map(([label, run]) => (0, import_react19.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), - deepseek ? (0, import_react19.createElement)(KeyHelpLink, { key: "key-help" }) : null + choices.map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), + deepseek ? (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" }) : null ) : null ); } @@ -11067,14 +11265,14 @@ function DeepSeekSignIn({ useUi, actions }) { const open = useUi((state) => state.signIn); const entry = open ? actions.signInEntry?.() : null; if (!entry) return null; - return (0, import_react19.createElement)(SignInHost, { key: open, entry, actions }); + return (0, import_react20.createElement)(SignInHost, { key: open, entry, actions }); } function SignInHost({ entry, actions }) { - const [ops] = (0, import_react19.useState)(() => entry.inject?.() ?? {}); + const [ops] = (0, import_react20.useState)(() => entry.inject?.() ?? {}); const close = () => actions.closeSignIn(); - const useHook = (source) => (selector) => (0, import_react19.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); - const [hooks] = (0, import_react19.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); - return (0, import_react19.createElement)(entry.component, { + const useHook = (source) => (selector) => (0, import_react20.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); + const [hooks] = (0, import_react20.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); + return (0, import_react20.createElement)(entry.component, { ...ops, ...hooks, t: actions.accountT(), @@ -11133,7 +11331,7 @@ var MODEL_CSS = ` `; // src/client/model-test.js -var import_react20 = require("react"); +var import_react21 = require("react"); var FAILURE_COPY = { NO_MODEL: "Pick a model first.", NO_KEY: "This model has no API key yet. Set one up in the model settings.", @@ -11158,12 +11356,12 @@ function testOutcome(result, modelName) { } var testModel = (id) => call("test-model", { id }); function ModelTestRow({ bot, modelName, test = testModel }) { - const [state, setState] = (0, import_react20.useState)({ busy: false, outcome: void 0 }); - const live = (0, import_react20.useRef)(true); - (0, import_react20.useEffect)(() => () => { + const [state, setState] = (0, import_react21.useState)({ busy: false, outcome: void 0 }); + const live = (0, import_react21.useRef)(true); + (0, import_react21.useEffect)(() => () => { live.current = false; }, []); - (0, import_react20.useEffect)(() => { + (0, import_react21.useEffect)(() => { setState({ busy: false, outcome: void 0 }); }, [bot.id, bot.model]); const start = () => { @@ -11177,21 +11375,21 @@ function ModelTestRow({ bot, modelName, test = testModel }) { }); }; const { busy, outcome } = state; - return (0, import_react20.createElement)( + return (0, import_react21.createElement)( "div", { className: "bt-set-row bt-bs-row bt-model-test" }, - (0, import_react20.createElement)( + (0, import_react21.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), - (0, import_react20.createElement)( + (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), + (0, import_react21.createElement)( "span", { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, - outcome?.code ? (0, import_react20.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null + outcome?.code ? (0, import_react21.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null ) ), - (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) + (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ); } var MODEL_TEST_CSS = ` @@ -11203,17 +11401,17 @@ var MODEL_TEST_CSS = ` `; // src/client/folder-menu.js -var import_react21 = require("react"); +var import_react22 = require("react"); var import_react_dom7 = require("react-dom"); var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); -var Tick = ({ on }) => (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react21.createElement)(CheckIcon) : null); +var Tick = ({ on }) => (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react22.createElement)(CheckIcon) : null); function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); - const [info, setInfo] = (0, import_react21.useState)(null); - const [view, setView] = (0, import_react21.useState)("list"); - const [draft, setDraft] = (0, import_react21.useState)(""); - const [listing, setListing] = (0, import_react21.useState)(null); - const [problem, setProblem] = (0, import_react21.useState)(""); + const [info, setInfo] = (0, import_react22.useState)(null); + const [view, setView] = (0, import_react22.useState)("list"); + const [draft, setDraft] = (0, import_react22.useState)(""); + const [listing, setListing] = (0, import_react22.useState)(null); + const [problem, setProblem] = (0, import_react22.useState)(""); const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { @@ -11246,35 +11444,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; - const listView = (0, import_react21.createElement)( + const listView = (0, import_react22.createElement)( "div", { key: "list", className: "bt-folder-view" }, - (0, import_react21.createElement)( + (0, import_react22.createElement)( "button", { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, - (0, import_react21.createElement)("span", { className: "bt-menu-label" }, fallback), - (0, import_react21.createElement)(Tick, { on: !value }) + (0, import_react22.createElement)("span", { className: "bt-menu-label" }, fallback), + (0, import_react22.createElement)(Tick, { on: !value }) ), - folders.length > 0 ? (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, - folders.map((folder) => (0, import_react21.createElement)( + folders.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, + folders.map((folder) => (0, import_react22.createElement)( "button", { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, - (0, import_react21.createElement)("span", { className: "bt-folder-copy" }, (0, import_react21.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react21.createElement)("span", { className: "bt-folder-path" }, folder.path)), - (0, import_react21.createElement)(Tick, { on: folder.path === value }) + (0, import_react22.createElement)("span", { className: "bt-folder-copy" }, (0, import_react22.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react22.createElement)("span", { className: "bt-folder-path" }, folder.path)), + (0, import_react22.createElement)(Tick, { on: folder.path === value }) )), - (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - info?.picker === "native" ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, - info?.picker === "browse" ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, - (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => { + (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + info?.picker === "native" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, + info?.picker === "browse" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, + (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => { setDraft(value ?? ""); setView("type"); - } }, (0, import_react21.createElement)(PencilIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) + } }, (0, import_react22.createElement)(PencilIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) ); - const typeView = (0, import_react21.createElement)( + const typeView = (0, import_react22.createElement)( "div", { key: "type", className: "bt-folder-view" }, - (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react21.createElement)(ChevronLeftIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Back"))), - (0, import_react21.createElement)("input", { + (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))), + (0, import_react22.createElement)("input", { className: "bt-folder-input", autoFocus: true, value: draft, @@ -11286,33 +11484,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), - (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) + (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) ); const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; - const browseView = listing ? (0, import_react21.createElement)( + const browseView = listing ? (0, import_react22.createElement)( "div", { key: `browse:${listing.path}`, className: "bt-folder-view" }, - (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react21.createElement)(ChevronLeftIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Back"))), - (0, import_react21.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), - parent ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, - inside.map((entry) => (0, import_react21.createElement)( + (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))), + (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), + parent ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, + inside.map((entry) => (0, import_react22.createElement)( "button", { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, - (0, import_react21.createElement)(FolderIcon), - (0, import_react21.createElement)("span", { className: "bt-menu-label" }, entry.name), - (0, import_react21.createElement)(ChevronRightIcon) + (0, import_react22.createElement)(FolderIcon), + (0, import_react22.createElement)("span", { className: "bt-menu-label" }, entry.name), + (0, import_react22.createElement)(ChevronRightIcon) )), - inside.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, - listing.truncated ? (0, import_react21.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, - (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) + inside.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, + listing.truncated ? (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, + (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) ) : null; const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; - return (0, import_react21.createElement)( - import_react21.Fragment, + return (0, import_react22.createElement)( + import_react22.Fragment, null, - (0, import_react21.createElement)( + (0, import_react22.createElement)( "button", { ref: button, @@ -11325,19 +11523,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } title: value, onClick: toggle }, - (0, import_react21.createElement)( + (0, import_react22.createElement)( "span", { className: "bt-model-copy" }, - (0, import_react21.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), - value ? (0, import_react21.createElement)("span", { className: "bt-model-provider" }, value) : null + (0, import_react22.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), + value ? (0, import_react22.createElement)("span", { className: "bt-model-provider" }, value) : null ), - (0, import_react21.createElement)(Caret) + (0, import_react22.createElement)(Caret) ), - shownPlace ? (0, import_react_dom7.createPortal)((0, import_react21.createElement)( + shownPlace ? (0, import_react_dom7.createPortal)((0, import_react22.createElement)( "div", { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, shownView, - problem ? (0, import_react21.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null + problem ? (0, import_react22.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null ), document.body) : null ); } @@ -11357,7 +11555,7 @@ var FOLDER_CSS = ` `; // src/client/schedules.js -var import_react22 = require("react"); +var import_react23 = require("react"); var POLL_MS5 = 3e4; var shared = null; var listeners = /* @__PURE__ */ new Set(); @@ -11366,8 +11564,8 @@ function publish(view) { for (const listener of listeners) listener(view); } function useSchedules(actions) { - const [view, setView] = (0, import_react22.useState)(shared); - (0, import_react22.useEffect)(() => { + const [view, setView] = (0, import_react23.useState)(shared); + (0, import_react23.useEffect)(() => { listeners.add(setView); let stopped = false; let timer; @@ -11439,9 +11637,9 @@ function lastRunText(run) { } } function TaskRow({ task, actions, onView, onEdit, showBot, roster }) { - const [confirming, setConfirming] = (0, import_react22.useState)(false); - const [busy, setBusy] = (0, import_react22.useState)(false); - (0, import_react22.useEffect)(() => { + const [confirming, setConfirming] = (0, import_react23.useState)(false); + const [busy, setBusy] = (0, import_react23.useState)(false); + (0, import_react23.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); @@ -11462,7 +11660,7 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) { onView(next); }, () => setBusy(false)); }; - const [retryError, setRetryError] = (0, import_react22.useState)(""); + const [retryError, setRetryError] = (0, import_react23.useState)(""); const retry = () => { setBusy(true); setRetryError(""); @@ -11478,24 +11676,24 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) { const stoppable = task.status === "active" || task.status === "paused"; const failed = task.lastRun?.result === "failed"; const last = lastRunText(task.lastRun); - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "div", { className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, - showBot ? (0, import_react22.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, - (0, import_react22.createElement)( + showBot ? (0, import_react23.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, + (0, import_react23.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react22.createElement)("span", { className: "bt-task-title" }, task.title), - (0, import_react22.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), - task.prompt && task.prompt !== task.title ? (0, import_react22.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, - last ? (0, import_react22.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, - retryError ? (0, import_react22.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null + (0, import_react23.createElement)("span", { className: "bt-task-title" }, task.title), + (0, import_react23.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), + task.prompt && task.prompt !== task.title ? (0, import_react23.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, + last ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, + retryError ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null ), - (0, import_react22.createElement)("span", { className: "bt-task-next" }, nextText(task)), - failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, - onEdit ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react22.createElement)(PencilIcon)) : null, - stoppable ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, - (0, import_react22.createElement)("button", { + (0, import_react23.createElement)("span", { className: "bt-task-next" }, nextText(task)), + failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, + onEdit ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react23.createElement)(PencilIcon)) : null, + stoppable ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, + (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, @@ -11503,7 +11701,7 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) { "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove - }, confirming ? t("Delete") : (0, import_react22.createElement)(TrashIcon)) + }, confirming ? t("Delete") : (0, import_react23.createElement)(TrashIcon)) ); } var pad = (number) => String(number).padStart(2, "0"); @@ -11536,17 +11734,17 @@ function formStart(task) { } function TaskForm({ roster, actions, start, task, onDone, onCancel }) { const first = formStart(task); - const [target, setTarget] = (0, import_react22.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? ""); - const [title, setTitle] = (0, import_react22.useState)(first.title); - const [prompt, setPrompt] = (0, import_react22.useState)(first.prompt); - const [kind, setKind] = (0, import_react22.useState)(first.kind); - const [at, setAt] = (0, import_react22.useState)(first.at); - const [time, setTime] = (0, import_react22.useState)(first.time); - const [weekdays, setWeekdays] = (0, import_react22.useState)(first.weekdays); - const [count, setCount] = (0, import_react22.useState)(first.count); - const [unit, setUnit] = (0, import_react22.useState)(first.unit); - const [busy, setBusy] = (0, import_react22.useState)(false); - const [error, setError] = (0, import_react22.useState)(""); + const [target, setTarget] = (0, import_react23.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? ""); + const [title, setTitle] = (0, import_react23.useState)(first.title); + const [prompt, setPrompt] = (0, import_react23.useState)(first.prompt); + const [kind, setKind] = (0, import_react23.useState)(first.kind); + const [at, setAt] = (0, import_react23.useState)(first.at); + const [time, setTime] = (0, import_react23.useState)(first.time); + const [weekdays, setWeekdays] = (0, import_react23.useState)(first.weekdays); + const [count, setCount] = (0, import_react23.useState)(first.count); + const [unit, setUnit] = (0, import_react23.useState)(first.unit); + const [busy, setBusy] = (0, import_react23.useState)(false); + const [error, setError] = (0, import_react23.useState)(""); const timing = () => { if (kind === "cron") return { kind: "keep" }; if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone }; @@ -11573,69 +11771,69 @@ function TaskForm({ roster, actions, start, task, onDone, onCancel }) { }); }; const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "form", { className: "bt-set-card bt-task-form", onSubmit: submit }, - (0, import_react22.createElement)( + (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), - (0, import_react22.createElement)( + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), + (0, import_react23.createElement)( "select", { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, - (0, import_react22.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react22.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), - roster.rooms.length > 0 ? (0, import_react22.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react22.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null + (0, import_react23.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react23.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), + roster.rooms.length > 0 ? (0, import_react23.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react23.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null ) ), - (0, import_react22.createElement)( + (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Name"))), - (0, import_react22.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), + (0, import_react23.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) ), - (0, import_react22.createElement)( + (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), - (0, import_react22.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), + (0, import_react23.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) ), - (0, import_react22.createElement)( + (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("When"))), - (0, import_react22.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react22.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), - (0, import_react22.createElement)( + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("When"))), + (0, import_react23.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react23.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), + (0, import_react23.createElement)( "div", { key: kind, className: "bt-task-when bt-veil" }, - kind === "cron" ? (0, import_react22.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, - kind === "at" ? (0, import_react22.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, - kind === "weekly" ? (0, import_react22.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react22.createElement)("button", { + kind === "cron" ? (0, import_react23.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, + kind === "at" ? (0, import_react23.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, + kind === "weekly" ? (0, import_react23.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react23.createElement)("button", { key: day, type: "button", className: "bt-task-day", "aria-pressed": weekdays.includes(day), onClick: () => toggleDay(day) }, when2(Date.UTC(2024, 0, day), { weekday: "narrow", timeZone: "UTC" })))) : null, - kind === "daily" || kind === "weekly" ? (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, - kind === "every" ? (0, import_react22.createElement)( - import_react22.Fragment, + kind === "daily" || kind === "weekly" ? (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, + kind === "every" ? (0, import_react23.createElement)( + import_react23.Fragment, null, - (0, import_react22.createElement)("span", { className: "bt-task-every" }, t("Every")), - (0, import_react22.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), - (0, import_react22.createElement)( + (0, import_react23.createElement)("span", { className: "bt-task-every" }, t("Every")), + (0, import_react23.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), + (0, import_react23.createElement)( "select", { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, - UNITS.map(([id, , label]) => (0, import_react22.createElement)("option", { key: id, value: id }, t(label))) + UNITS.map(([id, , label]) => (0, import_react23.createElement)("option", { key: id, value: id }, t(label))) ) ) : null ) ), - error ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, - (0, import_react22.createElement)( + error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, + (0, import_react23.createElement)( "div", { className: "bt-set-row bt-task-foot" }, - (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), - (0, import_react22.createElement)( + (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), + (0, import_react23.createElement)( "button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") @@ -11648,21 +11846,21 @@ function openPlugins(actions) { actions.selectPanel("plugins"); } function AutomationOff({ actions }) { - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "div", { className: "bt-set-card bt-task-off" }, - (0, import_react22.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react22.createElement)(ClockIcon)), - (0, import_react22.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), - (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), - (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) + (0, import_react23.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react23.createElement)(ClockIcon)), + (0, import_react23.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), + (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), + (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ); } function QuietHoursCard({ view, actions }) { const quiet = view.quietHours ?? { on: false }; - const [busy, setBusy] = (0, import_react22.useState)(false); - const [error, setError] = (0, import_react22.useState)(""); - const [from, setFrom] = (0, import_react22.useState)(quiet.from ?? "22:00"); - const [to, setTo] = (0, import_react22.useState)(quiet.to ?? "07:00"); + const [busy, setBusy] = (0, import_react23.useState)(false); + const [error, setError] = (0, import_react23.useState)(""); + const [from, setFrom] = (0, import_react23.useState)(quiet.from ?? "22:00"); + const [to, setTo] = (0, import_react23.useState)(quiet.to ?? "07:00"); const unchecked = view.quietUnchecked === true; const refresh = () => { void Promise.resolve(actions.schedules?.()).then((next) => { @@ -11680,23 +11878,23 @@ function QuietHoursCard({ view, actions }) { refresh(); }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); }; - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "section", { className: "bt-set-section" }, - (0, import_react22.createElement)("h3", null, t("Quiet hours")), - (0, import_react22.createElement)( + (0, import_react23.createElement)("h3", null, t("Quiet hours")), + (0, import_react23.createElement)( "div", { className: "bt-set-card" }, - (0, import_react22.createElement)( + (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react22.createElement)( + (0, import_react23.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react22.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), - (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) + (0, import_react23.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), + (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) ), - (0, import_react22.createElement)("button", { + (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, @@ -11704,30 +11902,30 @@ function QuietHoursCard({ view, actions }) { onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) }, quiet.on ? t("Turn off") : t("Turn on")) ), - quiet.on ? (0, import_react22.createElement)( + quiet.on ? (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Window"))), - (0, import_react22.createElement)( + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Window"))), + (0, import_react23.createElement)( "div", { className: "bt-task-when" }, - (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), - (0, import_react22.createElement)("span", { className: "bt-task-every" }, "–"), - (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), - (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) + (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), + (0, import_react23.createElement)("span", { className: "bt-task-every" }, "–"), + (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), + (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) ) ) : null, - error ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, - quiet.on && unchecked ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null + error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, + quiet.on && unchecked ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null ) ); } function SchedulesPage({ roster, actions, start }) { const view = useSchedules(actions); - const [adding, setAdding] = (0, import_react22.useState)(Boolean(start)); - const [editing, setEditing] = (0, import_react22.useState)(null); - if (view === null) return (0, import_react22.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); - if (!view.available) return (0, import_react22.createElement)(AutomationOff, { actions }); + const [adding, setAdding] = (0, import_react23.useState)(Boolean(start)); + const [editing, setEditing] = (0, import_react23.useState)(null); + if (view === null) return (0, import_react23.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); + if (!view.available) return (0, import_react23.createElement)(AutomationOff, { actions }); const shown = (next) => { publish(next); setAdding(false); @@ -11738,52 +11936,52 @@ function SchedulesPage({ roster, actions, start }) { setEditing(task); }; const byChat = [ - ...roster.bots.map((bot) => [bot.id, (0, import_react22.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), - ...roster.rooms.map((room2) => [room2.id, (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) + ...roster.bots.map((bot) => [bot.id, (0, import_react23.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), + ...roster.rooms.map((room2) => [room2.id, (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) ].filter(([, , , tasks]) => tasks.length > 0); return [ - (0, import_react22.createElement)(QuietHoursCard, { key: "quiet", view, actions }), - adding ? (0, import_react22.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react22.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { + (0, import_react23.createElement)(QuietHoursCard, { key: "quiet", view, actions }), + adding ? (0, import_react23.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react23.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { setEditing(null); setAdding(true); - } }, (0, import_react22.createElement)(PlusIcon), t("New task"))), - byChat.length === 0 && !adding ? (0, import_react22.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react22.createElement)( + } }, (0, import_react23.createElement)(PlusIcon), t("New task"))), + byChat.length === 0 && !adding ? (0, import_react23.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react23.createElement)( "section", { key: id, className: "bt-set-section" }, - (0, import_react22.createElement)("h3", { className: "bt-task-head" }, avatar, name), - (0, import_react22.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) + (0, import_react23.createElement)("h3", { className: "bt-task-head" }, avatar, name), + (0, import_react23.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) )) ]; } function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; - const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react22.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit })); - return (0, import_react22.createElement)( - import_react22.Fragment, + const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react23.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit })); + return (0, import_react23.createElement)( + import_react23.Fragment, null, - open ? (0, import_react22.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, - rows.length + extra.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null + open ? (0, import_react23.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, + rows.length + extra.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null ); } function BotSchedules({ bot, roster, actions }) { const view = useSchedules(actions); - const [editing, setEditing] = (0, import_react22.useState)(null); + const [editing, setEditing] = (0, import_react23.useState)(null); if (view === null) return null; const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "section", { className: "bt-set-section" }, - (0, import_react22.createElement)("h3", null, t("Scheduled tasks")), - !view.available ? (0, import_react22.createElement)( + (0, import_react23.createElement)("h3", null, t("Scheduled tasks")), + !view.available ? (0, import_react23.createElement)( "div", { className: "bt-set-card" }, - (0, import_react22.createElement)( + (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), - (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) + (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), + (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ) - ) : (0, import_react22.createElement)(TaskList, { + ) : (0, import_react23.createElement)(TaskList, { tasks, editing, roster, @@ -11794,23 +11992,23 @@ function BotSchedules({ bot, roster, actions }) { setEditing(null); }, onCancel: () => setEditing(null), - extra: [(0, import_react22.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react22.createElement)(PlusIcon), t("New task"))] + extra: [(0, import_react23.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react23.createElement)(PlusIcon), t("New task"))] }) ); } function ScheduleSummary({ bot, actions }) { const view = useSchedules(actions); const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; - return (0, import_react22.createElement)( + return (0, import_react23.createElement)( "div", null, - (0, import_react22.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), - tasks.length === 0 ? (0, import_react22.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react22.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react22.createElement)( + (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), + tasks.length === 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react23.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react23.createElement)( "button", { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, - (0, import_react22.createElement)(ClockIcon), - (0, import_react22.createElement)("span", { className: "bt-task-line-title" }, task.title), - (0, import_react22.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) + (0, import_react23.createElement)(ClockIcon), + (0, import_react23.createElement)("span", { className: "bt-task-line-title" }, task.title), + (0, import_react23.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) ))) ); } @@ -11860,7 +12058,7 @@ var SCHEDULE_CSS = ` `; // src/client/capability-settings.js -var import_react23 = require("react"); +var import_react24 = require("react"); var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); var parseMap = (text, label) => { let value; @@ -11874,11 +12072,11 @@ var parseMap = (text, label) => { }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { - return (0, import_react23.createElement)( + return (0, import_react24.createElement)( "label", { 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" }, t(label))), - (0, import_react23.createElement)(multiline ? "textarea" : "input", { + (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t(label))), + (0, import_react24.createElement)(multiline ? "textarea" : "input", { className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, @@ -11890,7 +12088,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { - const [error, setError] = (0, import_react23.useState)(""); + const [error, setError] = (0, import_react24.useState)(""); const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); @@ -11921,95 +12119,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); - return (0, import_react23.createElement)( + return (0, import_react24.createElement)( "form", { className: "bt-connector-form", onSubmit: submit }, - (0, import_react23.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), - (0, import_react23.createElement)( + (0, import_react24.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), + (0, import_react24.createElement)( "label", { 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" }, t("Type"))), - (0, import_react23.createElement)( + (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Type"))), + (0, import_react24.createElement)( "select", { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, - ["mcp", "skill", "pack"].map((kind) => (0, import_react23.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) + ["mcp", "skill", "pack"].map((kind) => (0, import_react24.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) ) ), - (0, import_react23.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), - draft.kind === "mcp" ? (0, import_react23.createElement)( + (0, import_react24.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), + draft.kind === "mcp" ? (0, import_react24.createElement)( "label", { 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" }, t("Connection"))), - (0, import_react23.createElement)( + (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), + (0, import_react24.createElement)( "select", { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, - (0, import_react23.createElement)("option", { value: "stdio" }, t("Local command")), - (0, import_react23.createElement)("option", { value: "streamable-http" }, t("HTTP server")) + (0, import_react24.createElement)("option", { value: "stdio" }, t("Local command")), + (0, import_react24.createElement)("option", { value: "streamable-http" }, t("HTTP server")) ) ) : null, - draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react23.createElement)( + draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react24.createElement)( "div", null, - (0, import_react23.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), - (0, import_react23.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), - (0, import_react23.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) + (0, import_react24.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), + (0, import_react24.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), + (0, import_react24.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ) : null, - draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react23.createElement)( + draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react24.createElement)( "div", null, - (0, import_react23.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), - (0, import_react23.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) + (0, import_react24.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), + (0, import_react24.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ) : null, - draft.kind === "skill" ? (0, import_react23.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, - draft.kind === "pack" ? (0, import_react23.createElement)( + draft.kind === "skill" ? (0, import_react24.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, + draft.kind === "pack" ? (0, import_react24.createElement)( "div", { className: "bt-set-card" }, - available.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react23.createElement)( + available.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react24.createElement)( "label", { key: item.name, 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" }, item.name)), - (0, import_react23.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) + (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, item.name)), + (0, import_react24.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) )) ) : null, - error ? (0, import_react23.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, - (0, import_react23.createElement)( + error ? (0, import_react24.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, + (0, import_react24.createElement)( "div", { className: "bt-connector-form-row" }, - (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), - (0, import_react23.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), + (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) ) ); } function CapabilityRow({ item, onEdit, onDelete, onApprove, confirming }) { const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; - return (0, import_react23.createElement)( + return (0, import_react24.createElement)( "div", { className: "bt-connector" }, - (0, import_react23.createElement)( + (0, import_react24.createElement)( "div", { className: "bt-connector-head" }, - (0, import_react23.createElement)( + (0, import_react24.createElement)( "span", { className: "bt-connector-text" }, - (0, import_react23.createElement)("span", { className: "bt-connector-name" }, item.name), - (0, import_react23.createElement)("span", { className: "bt-connector-status" }, secondary) + (0, import_react24.createElement)("span", { className: "bt-connector-name" }, item.name), + (0, import_react24.createElement)("span", { className: "bt-connector-status" }, secondary) ), - (0, import_react23.createElement)( + (0, import_react24.createElement)( "span", { className: "bt-connector-actions" }, - item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, - (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), - (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) + item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) ) ) ); } function CapabilitiesPage({ roster, actions, run }) { const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name)); - const [draft, setDraft] = (0, import_react23.useState)(null); - const [confirming, setConfirming] = (0, import_react23.useState)(""); - (0, import_react23.useEffect)(() => { + const [draft, setDraft] = (0, import_react24.useState)(null); + const [confirming, setConfirming] = (0, import_react24.useState)(""); + (0, import_react24.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); @@ -12032,20 +12230,20 @@ function CapabilitiesPage({ roster, actions, run }) { setConfirming(""); void run(actions.deleteCapability(item.name)); }; - return (0, import_react23.createElement)( + return (0, import_react24.createElement)( "section", { className: "bt-set-section" }, - (0, import_react23.createElement)("h3", null, t("Shared capabilities")), - (0, import_react23.createElement)( + (0, import_react24.createElement)("h3", null, t("Shared capabilities")), + (0, import_react24.createElement)( "div", { className: "bt-actions" }, - (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) ), - draft ? (0, import_react23.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, - (0, import_react23.createElement)( + draft ? (0, import_react24.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, + (0, import_react24.createElement)( "div", { className: "bt-set-card bt-connectors-card" }, - items.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react23.createElement)(CapabilityRow, { + items.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react24.createElement)(CapabilityRow, { key: item.name, item, confirming: confirming === item.name, @@ -12061,10 +12259,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow const attached = new Set(bot.capabilities ?? []); const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; - return (0, import_react23.createElement)( + return (0, import_react24.createElement)( Card2, { title: t("Capabilities") }, - items.map((item) => (0, import_react23.createElement)(SwitchRow2, { + items.map((item) => (0, import_react24.createElement)(SwitchRow2, { key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, @@ -12075,7 +12273,7 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow } // src/client/connector-bots.js -var import_react24 = require("react"); +var import_react25 = require("react"); function ownBots(roster) { const me = roster?.self?.memberId; return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); @@ -12091,37 +12289,37 @@ function usageText(roster, id) { return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); } function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { - return (0, import_react24.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); + return (0, import_react25.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); } function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { const bots = ownBots(roster); - return (0, import_react24.createElement)( + return (0, import_react25.createElement)( "div", { className: "bt-connector-bots" }, - bots.length === 0 ? (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react24.createElement)( + bots.length === 0 ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react25.createElement)( "div", { key: bot.id, className: "bt-connector-bot" }, - (0, import_react24.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) + (0, import_react25.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) )), - (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) + (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) ); } function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { const connected = connectedConnectors(roster); if (connected.length === 0) return null; - return (0, import_react24.createElement)( + return (0, import_react25.createElement)( Card2, { title: t("Connectors") }, - connected.map((connector) => (0, import_react24.createElement)( + connected.map((connector) => (0, import_react25.createElement)( "div", { key: connector.id, className: "bt-connector-bot" }, - (0, import_react24.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) + (0, import_react25.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) )) ); } // src/client/memory-panel.js -var import_react25 = require("react"); +var import_react26 = require("react"); var POLL_MS6 = 1500; var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); @@ -12132,8 +12330,8 @@ function noteCount(botId, count) { for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { - const [count, setCount] = (0, import_react25.useState)(counts.get(botId) ?? null); - (0, import_react25.useEffect)(() => { + const [count, setCount] = (0, import_react26.useState)(counts.get(botId) ?? null); + (0, import_react26.useEffect)(() => { const listener = () => setCount(counts.get(botId) ?? null); countListeners.add(listener); listener(); @@ -12152,16 +12350,16 @@ function useMemoryCount(botId, actions) { function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", null, - (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Memory")), - (0, import_react25.createElement)( + (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Memory")), + (0, import_react26.createElement)( "button", { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, - (0, import_react25.createElement)(MemoryIcon), - (0, import_react25.createElement)("span", { className: "bt-mem-row-copy" }, label), - (0, import_react25.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react25.createElement)(ChevronRightIcon)) + (0, import_react26.createElement)(MemoryIcon), + (0, import_react26.createElement)("span", { className: "bt-mem-row-copy" }, label), + (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon)) ) ); } @@ -12176,45 +12374,45 @@ function ago(at, now) { return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", { className: "bt-mem-skel", "aria-busy": true }, - [0, 1].map((block) => (0, import_react25.createElement)( + [0, 1].map((block) => (0, import_react26.createElement)( "div", { key: block, className: "bt-mem-skel-block" }, - (0, import_react25.createElement)("span", { className: "bt-mem-skel-head" }), - (0, import_react25.createElement)("span", null), - (0, import_react25.createElement)("span", null), - (0, import_react25.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) + (0, import_react26.createElement)("span", { className: "bt-mem-skel-head" }), + (0, import_react26.createElement)("span", null), + (0, import_react26.createElement)("span", null), + (0, import_react26.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) )), - label ? (0, import_react25.createElement)("div", { className: "bt-mem-skel-label" }, label) : null + label ? (0, import_react26.createElement)("div", { className: "bt-mem-skel-label" }, label) : null ); } function Summary({ bot, view, regenerate }) { if (view.entries.length === 0) { - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", { className: "bt-mem-empty" }, - (0, import_react25.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react25.createElement)(MemoryIcon)), - (0, import_react25.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), - (0, import_react25.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) + (0, import_react26.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react26.createElement)(MemoryIcon)), + (0, import_react26.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), + (0, import_react26.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) ); } - const failed = view.error && !view.summarizing ? (0, import_react25.createElement)( + const failed = view.error && !view.summarizing ? (0, import_react26.createElement)( "div", { className: "bt-mem-failed", role: "alert" }, - (0, import_react25.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), - (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) + (0, import_react26.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), + (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) ) : null; - if (!view.summary) return (0, import_react25.createElement)(import_react25.Fragment, null, failed, failed ? null : (0, import_react25.createElement)(Skeleton, { label: t("Writing the summary…") })); - return (0, import_react25.createElement)( - import_react25.Fragment, + if (!view.summary) return (0, import_react26.createElement)(import_react26.Fragment, null, failed, failed ? null : (0, import_react26.createElement)(Skeleton, { label: t("Writing the summary…") })); + return (0, import_react26.createElement)( + import_react26.Fragment, null, failed, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, - summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react25.createElement)("h3", { key: index }, block.text) : (0, import_react25.createElement)("p", { key: index }, block.text)) + summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react26.createElement)("h3", { key: index }, block.text) : (0, import_react26.createElement)("p", { key: index }, block.text)) ) ); } @@ -12226,9 +12424,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { return groups; }, []); function EntryRow({ entry, remove }) { - const [confirming, setConfirming] = (0, import_react25.useState)(false); - const [busy, setBusy] = (0, import_react25.useState)(false); - (0, import_react25.useEffect)(() => { + const [confirming, setConfirming] = (0, import_react26.useState)(false); + const [busy, setBusy] = (0, import_react26.useState)(false); + (0, import_react26.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); @@ -12243,16 +12441,16 @@ function EntryRow({ entry, remove }) { setBusy(true); if (!await remove(entry)) setBusy(false); }; - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "li", { className: "bt-mem-entry", "data-busy": busy || void 0 }, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-entry-copy" }, - (0, import_react25.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), - meta ? (0, import_react25.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null + (0, import_react26.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), + meta ? (0, import_react26.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null ), - (0, import_react25.createElement)("button", { + (0, import_react26.createElement)("button", { type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, @@ -12260,7 +12458,7 @@ function EntryRow({ entry, remove }) { "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click - }, confirming ? t("Remove") : (0, import_react25.createElement)(TrashIcon)) + }, confirming ? t("Remove") : (0, import_react26.createElement)(TrashIcon)) ); } function Entries({ bot, view, remove }) { @@ -12269,25 +12467,25 @@ function Entries({ bot, view, remove }) { { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", { className: "bt-mem-entries" }, - sections.map((section) => (0, import_react25.createElement)( + sections.map((section) => (0, import_react26.createElement)( "section", { key: section.scope, className: "bt-mem-section" }, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-section-head" }, - (0, import_react25.createElement)("h3", null, section.title), - section.hint ? (0, import_react25.createElement)("span", null, section.hint) : null, - (0, import_react25.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) + (0, import_react26.createElement)("h3", null, section.title), + section.hint ? (0, import_react26.createElement)("span", null, section.hint) : null, + (0, import_react26.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) ), - groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react25.createElement)( - import_react25.Fragment, + groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react26.createElement)( + import_react26.Fragment, { key: group.key }, - group.topic ? (0, import_react25.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, - (0, import_react25.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react25.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) - )) : (0, import_react25.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) + group.topic ? (0, import_react26.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, + (0, import_react26.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react26.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) + )) : (0, import_react26.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) )) ); } @@ -12299,40 +12497,40 @@ function changeLine(change) { return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); } function Reply({ answer, dismiss }) { - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", { className: "bt-mem-reply", role: "status" }, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-reply-head" }, - (0, import_react25.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), - answer.pending ? null : (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react25.createElement)(CloseIcon)) + (0, import_react26.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), + answer.pending ? null : (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react26.createElement)(CloseIcon)) ), - answer.pending ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react25.createElement)( - import_react25.Fragment, + answer.pending ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react26.createElement)( + import_react26.Fragment, null, - answer.reply ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, - answer.changes.length > 0 ? (0, import_react25.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react25.createElement)( + answer.reply ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, + answer.changes.length > 0 ? (0, import_react26.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react26.createElement)( "li", { key: index, "data-ok": change.ok || void 0 }, - change.ok ? (0, import_react25.createElement)(CheckIcon) : (0, import_react25.createElement)(CloseIcon), - (0, import_react25.createElement)("span", null, changeLine(change)) + change.ok ? (0, import_react26.createElement)(CheckIcon) : (0, import_react26.createElement)(CloseIcon), + (0, import_react26.createElement)("span", null, changeLine(change)) ))) : null ) ); } function useMemoryView(botId, actions) { - const [view, setView] = (0, import_react25.useState)(null); - const [error, setError] = (0, import_react25.useState)(""); - const timer = (0, import_react25.useRef)(0); - const latest = (0, import_react25.useRef)(0); - const alive = (0, import_react25.useRef)(true); - const show = (0, import_react25.useCallback)((next) => { + const [view, setView] = (0, import_react26.useState)(null); + const [error, setError] = (0, import_react26.useState)(""); + const timer = (0, import_react26.useRef)(0); + const latest = (0, import_react26.useRef)(0); + const alive = (0, import_react26.useRef)(true); + const show = (0, import_react26.useCallback)((next) => { setView(next); setError(""); noteCount(botId, next.entries.length); }, [botId]); - const load = (0, import_react25.useCallback)(async (flags = {}) => { + const load = (0, import_react26.useCallback)(async (flags = {}) => { clearTimeout(timer.current); const ticket = ++latest.current; try { @@ -12344,7 +12542,7 @@ function useMemoryView(botId, actions) { if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure)); } }, [botId]); - (0, import_react25.useEffect)(() => { + (0, import_react26.useEffect)(() => { alive.current = true; void load({ summarize: true }); return () => { @@ -12355,8 +12553,8 @@ function useMemoryView(botId, actions) { return { view, error, load, show }; } function useNow3(every) { - const [now, setNow] = (0, import_react25.useState)(Date.now()); - (0, import_react25.useEffect)(() => { + const [now, setNow] = (0, import_react26.useState)(Date.now()); + (0, import_react26.useEffect)(() => { const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); @@ -12364,18 +12562,18 @@ function useNow3(every) { } function MemoryDialog({ bot, actions, leaving = false }) { const { view, error, load, show } = useMemoryView(bot.id, actions); - const [page, setPage] = (0, import_react25.useState)("summary"); - const [menu, setMenu] = (0, import_react25.useState)(false); + const [page, setPage] = (0, import_react26.useState)("summary"); + const [menu, setMenu] = (0, import_react26.useState)(false); const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); - const [text, setText] = (0, import_react25.useState)(""); - const [answer, setAnswer] = (0, import_react25.useState)(null); - const menuRef = (0, import_react25.useRef)(null); - const dotsRef = (0, import_react25.useRef)(null); - const inputRef = (0, import_react25.useRef)(null); + const [text, setText] = (0, import_react26.useState)(""); + const [answer, setAnswer] = (0, import_react26.useState)(null); + const menuRef = (0, import_react26.useRef)(null); + const dotsRef = (0, import_react26.useRef)(null); + const inputRef = (0, import_react26.useRef)(null); const now = useNow3(3e4); - const close = (0, import_react25.useCallback)(() => actions.closeMemory(), []); + const close = (0, import_react26.useCallback)(() => actions.closeMemory(), []); useEscape(close); - (0, import_react25.useEffect)(() => { + (0, import_react26.useEffect)(() => { if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; @@ -12437,52 +12635,52 @@ function MemoryDialog({ bot, actions, leaving = false }) { const when3 = view?.summary ? ago(view.summary.at, now) : null; const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; const busy = answer?.pending === true; - return (0, import_react25.createElement)( + return (0, import_react26.createElement)( "div", { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, - (0, import_react25.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), - (0, import_react25.createElement)( + (0, import_react26.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react26.createElement)( "div", { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-head" }, - page === "entries" ? (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react25.createElement)(ChevronLeftIcon)) : null, - (0, import_react25.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), - (0, import_react25.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), - (0, import_react25.createElement)("span", { className: "bt-mem-head-gap" }), - (0, import_react25.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react25.createElement)(DotsIcon)), - (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react25.createElement)(CloseIcon)), - menuShown ? (0, import_react25.createElement)( + page === "entries" ? (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react26.createElement)(ChevronLeftIcon)) : null, + (0, import_react26.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), + (0, import_react26.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), + (0, import_react26.createElement)("span", { className: "bt-mem-head-gap" }), + (0, import_react26.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react26.createElement)(DotsIcon)), + (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react26.createElement)(CloseIcon)), + menuShown ? (0, import_react26.createElement)( "div", { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, - (0, import_react25.createElement)( + (0, import_react26.createElement)( "button", { type: "button", role: "menuitem", onClick: openEntries }, - (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("All entries")), - (0, import_react25.createElement)("span", { className: "bt-menu-hint" }, String(count)) + (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("All entries")), + (0, import_react26.createElement)("span", { className: "bt-menu-hint" }, String(count)) ), - (0, import_react25.createElement)( + (0, import_react26.createElement)( "button", { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, - (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) + (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) ) ) : null ), - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-body" }, - error ? (0, import_react25.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, - view === null ? error ? null : (0, import_react25.createElement)(Skeleton) : page === "entries" ? (0, import_react25.createElement)(Entries, { bot, view, remove }) : (0, import_react25.createElement)(Summary, { bot, view, regenerate }) + error ? (0, import_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, + view === null ? error ? null : (0, import_react26.createElement)(Skeleton) : page === "entries" ? (0, import_react26.createElement)(Entries, { bot, view, remove }) : (0, import_react26.createElement)(Summary, { bot, view, regenerate }) ), - (0, import_react25.createElement)( + (0, import_react26.createElement)( "div", { className: "bt-mem-foot" }, - answer ? (0, import_react25.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, - (0, import_react25.createElement)( + answer ? (0, import_react26.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, + (0, import_react26.createElement)( "form", { className: "bt-mem-ask", onSubmit: ask }, - (0, import_react25.createElement)("input", { + (0, import_react26.createElement)("input", { ref: inputRef, className: "bt-mem-input", value: text, @@ -12492,10 +12690,10 @@ function MemoryDialog({ bot, actions, leaving = false }) { "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), onChange: (event) => setText(event.target.value) }), - (0, import_react25.createElement)( + (0, import_react26.createElement)( "button", { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, - busy ? (0, import_react25.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react25.createElement)(ArrowUpIcon) + busy ? (0, import_react26.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react26.createElement)(ArrowUpIcon) ) ) ) @@ -12504,7 +12702,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { } // src/client/standing-approvals.js -var import_react26 = require("react"); +var import_react27 = require("react"); var standingOf = (target, roster) => { const me = roster?.self?.memberId; return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); @@ -12513,19 +12711,19 @@ function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; if (grants.length === 0) return null; const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); - return (0, import_react26.createElement)( + return (0, import_react27.createElement)( Card2, { title: t("Always allowed") }, - grants.map((grant) => (0, import_react26.createElement)( + grants.map((grant) => (0, import_react27.createElement)( "div", { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, - (0, import_react26.createElement)( + (0, import_react27.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react26.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), - (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) + (0, import_react27.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), + (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) ), - (0, import_react26.createElement)("button", { + (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), @@ -12536,7 +12734,7 @@ function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card } // src/client/bot-settings.js -var import_react27 = require("react"); +var import_react28 = require("react"); var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); @@ -12560,8 +12758,8 @@ async function avatarImage(file) { throw new Error(t("That image is too large")); } function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { - const [draft, setDraft] = (0, import_react27.useState)(null); - const reverting = (0, import_react27.useRef)(false); + const [draft, setDraft] = (0, import_react28.useState)(null); + const reverting = (0, import_react28.useRef)(false); const commit = () => { const next = draft?.trim(); setDraft(null); @@ -12572,7 +12770,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu if (next === void 0 || next === (value ?? "") || required && next === "") return; void onSave(next); }; - return (0, import_react27.createElement)(multiline ? "textarea" : "input", { + return (0, import_react28.createElement)(multiline ? "textarea" : "input", { className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, @@ -12593,29 +12791,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu }); } function Row({ label, hint, stack = false, children }) { - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( "label", { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) : null), + (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), children ); } function Card({ title, children }) { - return (0, import_react27.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react27.createElement)("h3", null, title) : null, (0, import_react27.createElement)("div", { className: "bt-set-card" }, children)); + return (0, import_react28.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react28.createElement)("h3", null, title) : null, (0, import_react28.createElement)("div", { className: "bt-set-card" }, children)); } function Switch({ on, label, disabled, onChange }) { - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( "button", { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, - (0, import_react27.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) + (0, import_react28.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ); } function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) : null), - (0, import_react27.createElement)(Switch, { on, label, disabled, onChange }) + (0, import_react28.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), + (0, import_react28.createElement)(Switch, { on, label, disabled, onChange }) ); } function modelHint(bot, roster) { @@ -12626,22 +12824,22 @@ function modelHint(bot, roster) { return t("Applies from the next message."); } function ShapeRing({ shape, size }) { - const maskId = (0, import_react27.useRef)(""); + const maskId = (0, import_react28.useRef)(""); if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; const def = shapeOf(shape); const unit = def.size / size; const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; - return (0, import_react27.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); + return (0, import_react28.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); } function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; if (!shapes.includes(look.shape)) shapes.push(look.shape); const rows = []; for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); - return (0, import_react27.createElement)( - import_react27.Fragment, + return (0, import_react28.createElement)( + import_react28.Fragment, null, - (0, import_react27.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react27.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react27.createElement)("button", { + (0, import_react28.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react28.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react28.createElement)("button", { key: id, type: "button", className: "bt-shape", @@ -12649,8 +12847,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { title: t(shapeOf(id).label), "aria-pressed": !hasPhoto && look.shape === id, onClick: () => onShape(id) - }, (0, import_react27.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react27.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), - (0, import_react27.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react27.createElement)("button", { + }, (0, import_react28.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react28.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), + (0, import_react28.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react28.createElement)("button", { key: color, type: "button", className: "bt-color", @@ -12658,19 +12856,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { title: t(capitalize(color)), "aria-pressed": look.color === color, onClick: () => onColor(color) - }, (0, import_react27.createElement)("span", { style: { background: paintCss(color) } })))) + }, (0, import_react28.createElement)("span", { style: { background: paintCss(color) } })))) ); } function AvatarEditor({ bot, actions, run, onClose }) { const look = lookOf(bot); - const [tab, setTab] = (0, import_react27.useState)(look.image ? "upload" : "bot"); - const [over, setOver] = (0, import_react27.useState)(false); - const ref = (0, import_react27.useRef)(null); - const fileRef = (0, import_react27.useRef)(null); + const [tab, setTab] = (0, import_react28.useState)(look.image ? "upload" : "bot"); + const [over, setOver] = (0, import_react28.useState)(false); + const ref = (0, import_react28.useRef)(null); + const fileRef = (0, import_react28.useRef)(null); const upload = (file) => { if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); }; - (0, import_react27.useEffect)(() => { + (0, import_react28.useEffect)(() => { const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); @@ -12688,7 +12886,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { window.removeEventListener("keydown", onKey, true); }; }, [onClose]); - (0, import_react27.useEffect)(() => { + (0, import_react28.useEffect)(() => { if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); @@ -12702,32 +12900,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { }, [tab, bot]); const hasPhoto = Boolean(look.image); const glide = useGlide(tab); - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( "div", { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-avedit-top" }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, - (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react27.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) + (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react28.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) ), - (0, import_react27.createElement)("button", { + (0, import_react28.createElement)("button", { type: "button", className: "bt-avedit-tab bt-avedit-reset", title: t("Reset character to default"), onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) }, t("Reset")) ), - tab === "bot" ? (0, import_react27.createElement)(CharacterGrid, { + tab === "bot" ? (0, import_react28.createElement)(CharacterGrid, { look, hasPhoto, onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), onColor: (color) => run(actions.updateBot(bot.id, { color })) }) : null, - tab === "upload" ? (0, import_react27.createElement)( + tab === "upload" ? (0, import_react28.createElement)( "div", { className: "bt-dropzone", @@ -12743,15 +12941,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { upload(event.dataTransfer.files?.[0]); } }, - hasPhoto ? (0, import_react27.createElement)("img", { src: look.image, alt: "" }) : null, - (0, import_react27.createElement)("div", null, t("Drag, drop, or paste an image")), - (0, import_react27.createElement)( + hasPhoto ? (0, import_react28.createElement)("img", { src: look.image, alt: "" }) : null, + (0, import_react28.createElement)("div", null, t("Drag, drop, or paste an image")), + (0, import_react28.createElement)( "div", { className: "bt-actions", style: { justifyContent: "center" } }, - (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), - hasPhoto ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null + (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), + hasPhoto ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null ), - (0, import_react27.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { + (0, import_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { const file = event.target.files?.[0]; event.target.value = ""; upload(file); @@ -12762,22 +12960,22 @@ function AvatarEditor({ bot, actions, run, onClose }) { function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( Card, { title: t("Memory") }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "button", { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label)), - (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon)) + (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label)), + (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon)) ) ); } function BotSettingsForm({ bot, roster, actions, run }) { - const [editing, setEditing] = (0, import_react27.useState)(false); - const [confirming, setConfirming] = (0, import_react27.useState)(false); - const closeEditor = (0, import_react27.useCallback)(() => setEditing(false), []); - (0, import_react27.useEffect)(() => { + const [editing, setEditing] = (0, import_react28.useState)(false); + const [confirming, setConfirming] = (0, import_react28.useState)(false); + const closeEditor = (0, import_react28.useCallback)(() => setEditing(false), []); + (0, import_react28.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 4e3); return () => clearTimeout(timer); @@ -12792,116 +12990,116 @@ function BotSettingsForm({ bot, roster, actions, run }) { if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; - (0, import_react27.useEffect)(() => { + (0, import_react28.useEffect)(() => { void actions.refreshModels?.(); }, [bot.id]); - return (0, import_react27.createElement)( + return (0, import_react28.createElement)( "div", { className: "bt-bs" }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-bs-hero" }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-avatar-anchor" }, - (0, import_react27.createElement)( + (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_react27.createElement)(BotMark, { bot, size: 64, live: true }), - (0, import_react27.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react27.createElement)(PencilIcon)) + (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_react27.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null + editing ? (0, import_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ), - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-bs-who" }, - (0, import_react27.createElement)("span", { className: "bt-bs-name" }, bot.name), - (0, import_react27.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) + (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_react27.createElement)( + (0, import_react28.createElement)( Card, { title: t("Profile") }, - (0, import_react27.createElement)(Row, { label: t("Name") }, (0, import_react27.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), - (0, import_react27.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react27.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) + (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_react27.createElement)( + (0, import_react28.createElement)( Card, { title: t("Model") }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), - (0, import_react27.createElement)(ModelMenu, { bot, roster, actions, run }) + (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_react27.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null + bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null ), - (0, import_react27.createElement)( + (0, import_react28.createElement)( Card, { title: t("Working folder") }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), - (0, import_react27.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) + (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_react27.createElement)( + (0, import_react28.createElement)( Card, { title: t("Instructions") }, - (0, import_react27.createElement)( + (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_react27.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) + (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 }) }) ) ), // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). - (0, import_react27.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react27.createElement)(Card, { title: t("Sharing") }, children) }), - (0, import_react27.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), - (0, import_react27.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), - roster.memory === false ? null : (0, import_react27.createElement)(MemoryCard, { bot, actions }), - (0, import_react27.createElement)(BotSchedules, { bot, roster, actions }), - (0, import_react27.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), - (0, import_react27.createElement)( + (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 }), + (0, import_react28.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), + roster.memory === false ? null : (0, import_react28.createElement)(MemoryCard, { bot, actions }), + (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_react27.createElement)( + onlyMain ? (0, import_react28.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react27.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_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_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) - ) : (0, import_react27.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)) }) + (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_react27.createElement)( + isTile ? null : (0, import_react28.createElement)( Card, { title: t("Sidebar") }, - (0, import_react27.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_react27.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_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_react27.createElement)( + (0, import_react28.createElement)( Card, { title: t("More") }, - (0, import_react27.createElement)( + (0, import_react28.createElement)( "button", { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, - (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), - (0, import_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon)) + (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_react27.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_react27.createElement)( + (0, import_react28.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react27.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), - (0, import_react27.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_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_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon)) + (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) ) ) ); @@ -12942,7 +13140,7 @@ ${MODEL_TEST_CSS} `; // src/client/group-settings.js -var import_react28 = require("react"); +var import_react29 = require("react"); var ROOM_MODES = [ ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], ["everyone", "Everyone", "Every member answers in turn, admin first."] @@ -12974,9 +13172,9 @@ function faceMarks(person, roster, size) { for (const seat of person?.seats ?? []) { const bot = faceSeatBot(seat, roster); if (!bot) continue; - marks.push((0, import_react28.createElement)(BotMark, { key: seat.key, bot, size, live: false })); + marks.push((0, import_react29.createElement)(BotMark, { key: seat.key, bot, size, live: false })); } - if (marks.length === 0) return [(0, import_react28.createElement)(PersonMark, { key: "person", size, name: person?.name })]; + if (marks.length === 0) return [(0, import_react29.createElement)(PersonMark, { key: "person", size, name: person?.name })]; return marks; } function faceRoomCount(room2, roster) { @@ -12990,31 +13188,31 @@ var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLow var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { const bots = person.seats.flatMap((seat, index) => { - const name = seat.local && onOpenBot ? (0, import_react28.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react28.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); - const marked = isLeadSeat(seat, lead) ? [name, (0, import_react28.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; - return index === 0 ? marked : [(0, import_react28.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; + const name = seat.local && onOpenBot ? (0, import_react29.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react29.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); + const marked = isLeadSeat(seat, lead) ? [name, (0, import_react29.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; + return index === 0 ? marked : [(0, import_react29.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; }); - return (0, import_react28.createElement)( + return (0, import_react29.createElement)( "div", { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "span", { className: "bt-gs-who bt-face-person" }, - (0, import_react28.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), - (0, import_react28.createElement)( + (0, import_react29.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), + (0, import_react29.createElement)( "span", { className: "bt-face-person-text" }, - (0, import_react28.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), - (0, import_react28.createElement)("span", { className: "bt-face-bots" }, bots) + (0, import_react29.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), + (0, import_react29.createElement)("span", { className: "bt-face-bots" }, bots) ) ) ); } function FaceLead({ room: room2, roster, pick: pick2 }) { const lead = room2.face?.lead; - const [open, setOpen] = (0, import_react28.useState)(false); - const [busy, setBusy] = (0, import_react28.useState)(false); - const [error, setError] = (0, import_react28.useState)(""); + const [open, setOpen] = (0, import_react29.useState)(false); + const [busy, setBusy] = (0, import_react29.useState)(false); + const [error, setError] = (0, import_react29.useState)(""); const seats = faceSeats(room2, roster); const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); if (!current) return null; @@ -13031,30 +13229,30 @@ function FaceLead({ room: room2, roster, pick: pick2 }) { setBusy(false); } }; - return (0, import_react28.createElement)( + return (0, import_react29.createElement)( "div", { className: "bt-face-lead", "data-flybot": "face-lead" }, - (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Room lead")), - (0, import_react28.createElement)( + (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Room lead")), + (0, import_react29.createElement)( "div", { className: "bt-set-row" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react28.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), - (0, import_react28.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) + (0, import_react29.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) ), - canPick ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null + canPick ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null ), - open && canPick ? seats.map((seat) => (0, import_react28.createElement)( + open && canPick ? seats.map((seat) => (0, import_react29.createElement)( "div", { key: seat.key, className: "bt-set-row" }, - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), - seat === current ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), + seat === current ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) )) : null, - open && canPick && lead.picked ? (0, import_react28.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, - canPick || !lead.supported ? null : (0, import_react28.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), - error ? (0, import_react28.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null + open && canPick && lead.picked ? (0, import_react29.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, + canPick || !lead.supported ? null : (0, import_react29.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), + error ? (0, import_react29.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null ); } function seatedCount(room2) { @@ -13070,9 +13268,9 @@ function roomSubtitle(room2, roster) { return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); } function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { - const [adding, setAdding] = (0, import_react28.useState)(false); - const [confirming, setConfirming] = (0, import_react28.useState)(false); - (0, import_react28.useEffect)(() => { + const [adding, setAdding] = (0, import_react29.useState)(false); + const [confirming, setConfirming] = (0, import_react29.useState)(false); + (0, import_react29.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 4e3); return () => clearTimeout(timer); @@ -13110,41 +13308,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { if (done) onDeleted?.(); }); }; - return (0, import_react28.createElement)( + return (0, import_react29.createElement)( "div", { className: "bt-bs" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "div", { className: "bt-bs-hero" }, - (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - (0, import_react28.createElement)( + (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), + (0, import_react29.createElement)( "div", { className: "bt-bs-who" }, - (0, import_react28.createElement)("span", { className: "bt-bs-name" }, room2.name), - (0, import_react28.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) + (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_react28.createElement)( + (0, import_react29.createElement)( Card, { title: t("Profile") }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( Row, { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, - (0, import_react28.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) + (0, import_react29.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) ), - (0, import_react28.createElement)( + (0, import_react29.createElement)( Row, { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, - (0, import_react28.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_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_react28.createElement)( + (0, import_react29.createElement)( Card, { title: t("Who answers") }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "div", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react28.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react28.createElement)("button", { + (0, import_react29.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react29.createElement)("button", { key: id, type: "button", "aria-pressed": mode === id, @@ -13154,108 +13352,108 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { if (mode !== id) void update({ mode: id }); } }, t(label)))), - (0, import_react28.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) + (0, import_react29.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) ) ), - (0, import_react28.createElement)( + (0, import_react29.createElement)( Card, { title: t("Working folder") }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ 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 }) }) ) ), - face ? (0, import_react28.createElement)( + face ? (0, import_react29.createElement)( Card, { title: t("Members · {count}", { count: people2.length }) }, - people2.map((person) => (0, import_react28.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) + 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_react28.createElement)( + face ? null : (0, import_react29.createElement)( Card, { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, - humans.map((person) => (0, import_react28.createElement)( + humans.map((person) => (0, import_react29.createElement)( "div", { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "span", { className: "bt-gs-who" }, - (0, import_react28.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), - (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Human")), - person.role === "admin" ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Admin")) : null + (0, import_react29.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), + (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Human")), + person.role === "admin" ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null ), - humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null + humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null )), members.map((member) => { const leads = room2.admin === member.id; const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; const taskLead = room2.taskLead === member.id; - return (0, import_react28.createElement)( + return (0, import_react29.createElement)( "div", { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "button", { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, - (0, import_react28.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), - (0, import_react28.createElement)("span", { className: "bt-row-title" }, member.name), - leads ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, - taskLead ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, - (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Chief")) : null + (0, import_react29.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), + (0, import_react29.createElement)("span", { className: "bt-row-title" }, member.name), + leads ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, + taskLead ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, + (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Chief")) : null ), - leads ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), - boardHere ? taskLead ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, - members.length > 2 ? (0, import_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null + leads ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), + boardHere ? taskLead ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, + members.length > 2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null ); }), - remotes.map((person) => (0, import_react28.createElement)( + remotes.map((person) => (0, import_react29.createElement)( "div", { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "span", { className: "bt-gs-who" }, - (0, import_react28.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), - (0, import_react28.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), + (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), + (0, import_react29.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is // labelled as such; a Bot with no Bot Social identity can never be written to at all. - room2.origin === "face-to-face" ? null : (0, import_react28.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) + 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")) ), // 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_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") + room2.origin === "face-to-face" || 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 }] }) }, "×") )), room2.origin === "face-to-face" ? null : adding ? [ - ...outsidePeople.map((person) => (0, import_react28.createElement)( + ...outsidePeople.map((person) => (0, import_react29.createElement)( "button", { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { setAdding(false); void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); } }, - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), - (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add person")) + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add person")) )), - ...outside.map((bot) => (0, import_react28.createElement)( + ...outside.map((bot) => (0, import_react29.createElement)( "button", { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { setAdding(false); void update({ add: [bot.id] }); } }, - (0, import_react28.createElement)(BotMark, { bot, size: 22 }), - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, bot.name)), - (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add")) + (0, import_react29.createElement)(BotMark, { bot, size: 22 }), + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, bot.name)), + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add")) )), - ...outsideRemotes.map((bot) => (0, import_react28.createElement)( + ...outsideRemotes.map((bot) => (0, import_react29.createElement)( "button", { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { setAdding(false); void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); } }, - (0, import_react28.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), - (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) + (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) )), - (0, import_react28.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) - ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react28.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react28.createElement)(PlusIcon), t("Add member")) : null, - (0, import_react28.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react28.createElement)( + (0, import_react29.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) + ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react29.createElement)(PlusIcon), t("Add member")) : null, + (0, import_react29.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react29.createElement)( "span", { className: "bt-set-hint" }, // The host checks for two members on every change, even a rename. @@ -13263,37 +13461,37 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { )), // #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_react28.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) + room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react29.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) ), - (0, import_react28.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), - (0, import_react28.createElement)( + (0, import_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), + (0, import_react29.createElement)( Card, { title: t("Sidebar") }, - (0, import_react28.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_react28.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_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_react28.createElement)( + (0, import_react29.createElement)( Card, { title: t("More") }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "button", { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react28.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), - (0, import_react28.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_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), + (0, import_react29.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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) + (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) ) ) ); } function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { - const [admin, setAdmin] = (0, import_react28.useState)(null); - const [members, setMembers] = (0, import_react28.useState)([]); - const [name, setName] = (0, import_react28.useState)(""); - const [busy, setBusy] = (0, import_react28.useState)(false); + const [admin, setAdmin] = (0, import_react29.useState)(null); + const [members, setMembers] = (0, import_react29.useState)([]); + const [name, setName] = (0, import_react29.useState)(""); + const [busy, setBusy] = (0, import_react29.useState)(false); const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); const pickAdmin = (id) => { setAdmin(id); @@ -13309,48 +13507,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { if (done && room2) onCreated(room2); else setBusy(false); }; - const chip = (bot, on, onClick, role) => (0, import_react28.createElement)("button", { + const chip = (bot, on, onClick, role) => (0, import_react29.createElement)("button", { key: bot.id, type: "button", role, className: "bt-ng-chip", "aria-checked": on, onClick - }, (0, import_react28.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react28.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react28.createElement)(AdminStar) : null); - return (0, import_react28.createElement)( + }, (0, import_react29.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react29.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react29.createElement)(AdminStar) : null); + return (0, import_react29.createElement)( "div", { className: "bt-set-card bt-ng" }, - (0, import_react28.createElement)( + (0, import_react29.createElement)( "div", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react28.createElement)(AdminStar), t("Choose the group admin"))), - (0, import_react28.createElement)( + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react29.createElement)(AdminStar), t("Choose the group admin"))), + (0, import_react29.createElement)( "div", { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) ) ), - admin === null ? null : (0, import_react28.createElement)( + admin === null ? null : (0, import_react29.createElement)( "div", { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), - (0, import_react28.createElement)( + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), + (0, import_react29.createElement)( "div", { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) ) ), - admin === null || members.length === 0 ? null : (0, import_react28.createElement)( + admin === null || members.length === 0 ? null : (0, import_react29.createElement)( "label", { className: "bt-set-row bt-bs-row bt-ng-step" }, - (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Name"))), - (0, import_react28.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Name"))), + (0, import_react29.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) ), - (0, import_react28.createElement)( + (0, import_react29.createElement)( "div", { className: "bt-set-row bt-task-foot" }, - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), - (0, import_react28.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) + (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), + (0, import_react29.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ) ); } @@ -13386,13 +13584,13 @@ button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-unde `; // src/client/palette.js -var import_react29 = require("react"); +var import_react30 = require("react"); var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); var PALETTE_EXIT_MS = 160; function CommandPalette({ start, token, roster, actions, useSessions }) { - const [query, setQuery] = (0, import_react29.useState)(start ?? ""); - const [cursor, setCursor] = (0, import_react29.useState)(0); - const [leaving, setLeaving] = (0, import_react29.useState)(false); + const [query, setQuery] = (0, import_react30.useState)(start ?? ""); + const [cursor, setCursor] = (0, import_react30.useState)(0); + const [leaving, setLeaving] = (0, import_react30.useState)(false); const wanted = query.trim().toLowerCase(); const close = () => { if (leaving) return; @@ -13428,7 +13626,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { const people2 = [ ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ key: bot.id, - avatar: (0, import_react29.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), + avatar: (0, import_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), title: pickerBotLabel(bot), subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), badge: bot.hidden ? t("Hidden") : void 0, @@ -13439,7 +13637,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { })), ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ key: room2.id, - avatar: (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), + avatar: (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), title: room2.name, subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), badge: room2.hidden ? t("Hidden") : void 0, @@ -13452,7 +13650,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { if (people2.length) sections.push([t("Bots"), people2]); const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ key: agent.id, - avatar: (0, import_react29.createElement)("span", { className: "bt-palette-agent" }, (0, import_react29.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), + avatar: (0, import_react30.createElement)("span", { className: "bt-palette-agent" }, (0, import_react30.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), title: "DSH Agent", subtitle: t("Plain dsh session"), run: () => actions.openAgent(agent.id) @@ -13465,16 +13663,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { key: `${id}:${turn}`, title: bot?.name ?? room2?.name ?? t("Bot"), subtitle: snippet, - avatar: bot ? (0, import_react29.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), + avatar: bot ? (0, import_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), run: () => actions.openSession(id) }; }); if (messages.length) sections.push([t("Messages"), messages]); const commands = [ - { key: "cmd:new", icon: (0, import_react29.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, - { key: "cmd:bot", icon: (0, import_react29.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, - { key: "cmd:group", icon: (0, import_react29.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, - { key: "cmd:settings", icon: (0, import_react29.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") } + { key: "cmd:new", icon: (0, import_react30.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, + { key: "cmd:bot", icon: (0, import_react30.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, + { key: "cmd:group", icon: (0, import_react30.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, + { key: "cmd:settings", icon: (0, import_react30.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") } ].filter((command) => matchesName(command.title)); if (commands.length) sections.push([t("Commands"), commands]); const rows = sections.flatMap(([, items]) => items); @@ -13484,7 +13682,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { ${shown}`, { axis: "y", ...QUICK_EDGES }); const listRef = glide.box; useFlip(listRef, shown, "[data-flip]"); - (0, import_react29.useEffect)(() => { + (0, import_react30.useEffect)(() => { listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); }, [active, query]); const choose = (row) => { @@ -13493,18 +13691,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); close(); }; let index = -1; - return (0, import_react29.createElement)( + return (0, import_react30.createElement)( "div", { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, - (0, import_react29.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), - (0, import_react29.createElement)( + (0, import_react30.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), + (0, import_react30.createElement)( "div", { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, - (0, import_react29.createElement)( + (0, import_react30.createElement)( "div", { className: "bt-palette-head" }, - (0, import_react29.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react29.createElement)(SearchIcon)), - (0, import_react29.createElement)("input", { + (0, import_react30.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)), + (0, import_react30.createElement)("input", { autoFocus: true, value: query, placeholder: t("Search Bots, chats and messages"), @@ -13530,29 +13728,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); } } }), - query ? (0, import_react29.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { + query ? (0, import_react30.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { setQuery(""); setCursor(0); - } }, (0, import_react29.createElement)(CloseIcon)) : null + } }, (0, import_react30.createElement)(CloseIcon)) : null ), - (0, import_react29.createElement)( + (0, import_react30.createElement)( "div", { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, - (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - rows.length === 0 ? (0, import_react29.createElement)( + (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + rows.length === 0 ? (0, import_react30.createElement)( "div", { className: "bt-palette-empty" }, - (0, import_react29.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react29.createElement)(SearchIcon)), - (0, import_react29.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), - (0, import_react29.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) - ) : sections.map(([title, items]) => (0, import_react29.createElement)( - import_react29.Fragment, + (0, import_react30.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)), + (0, import_react30.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), + (0, import_react30.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) + ) : sections.map(([title, items]) => (0, import_react30.createElement)( + import_react30.Fragment, { key: title }, - (0, import_react29.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), + (0, import_react30.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), items.map((row) => { index += 1; const at = index; - return (0, import_react29.createElement)( + return (0, import_react30.createElement)( "button", { key: row.key, @@ -13566,14 +13764,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); }, onClick: () => choose(row) }, - (0, import_react29.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react29.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), - (0, import_react29.createElement)( + (0, import_react30.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react30.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), + (0, import_react30.createElement)( "span", { className: "bt-palette-text" }, - (0, import_react29.createElement)("span", { className: "bt-palette-title" }, row.title), - row.subtitle ? (0, import_react29.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null + (0, import_react30.createElement)("span", { className: "bt-palette-title" }, row.title), + row.subtitle ? (0, import_react30.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null ), - row.badge ? (0, import_react29.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null + row.badge ? (0, import_react30.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null ); }) )) @@ -13615,7 +13813,7 @@ async function afterCreate({ id, text, actions }) { } // src/client/create-bot.js -var import_react30 = require("react"); +var import_react31 = require("react"); var pick = (list) => list[Math.floor(Math.random() * list.length)]; var randomLook = (avoid = {}) => { const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); @@ -13623,17 +13821,17 @@ var randomLook = (avoid = {}) => { return { shape: pick(shapes), color: pick(colors) }; }; function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { - const [look, setLook] = (0, import_react30.useState)(() => randomLook()); - const [image, setImage] = (0, import_react30.useState)(null); - const [name, setName] = (0, import_react30.useState)(start?.name ?? ""); - const [model, setModel] = (0, import_react30.useState)(() => startModel(roster)); - const [effort, setEffort] = (0, import_react30.useState)(void 0); - const [busy, setBusy] = (0, import_react30.useState)(false); - const [error, setError] = (0, import_react30.useState)(""); - const fileRef = (0, import_react30.useRef)(null); - const frame = (0, import_react30.useRef)(null); + const [look, setLook] = (0, import_react31.useState)(() => randomLook()); + const [image, setImage] = (0, import_react31.useState)(null); + const [name, setName] = (0, import_react31.useState)(start?.name ?? ""); + const [model, setModel] = (0, import_react31.useState)(() => startModel(roster)); + const [effort, setEffort] = (0, import_react31.useState)(void 0); + const [busy, setBusy] = (0, import_react31.useState)(false); + const [error, setError] = (0, import_react31.useState)(""); + const fileRef = (0, import_react31.useRef)(null); + const frame = (0, import_react31.useRef)(null); useEscape(onBack); - (0, import_react30.useEffect)(() => { + (0, import_react31.useEffect)(() => { if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); @@ -13658,25 +13856,25 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; - const card = (0, import_react30.createElement)( + const card = (0, import_react31.createElement)( "div", { className: "bt-create-card" }, - (0, import_react30.createElement)( + (0, import_react31.createElement)( "div", { className: "bt-create-preview" }, // The key remounts the mark, so every new pick plays the pop. - (0, import_react30.createElement)( + (0, import_react31.createElement)( "span", { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, - (0, import_react30.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) + (0, import_react31.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) ), - (0, import_react30.createElement)("span", { className: "bt-create-name" }, finalName) + (0, import_react31.createElement)("span", { className: "bt-create-name" }, finalName) ), - (0, import_react30.createElement)("div", { className: "bt-create-step" }, t("Look")), - (0, import_react30.createElement)( + (0, import_react31.createElement)("div", { className: "bt-create-step" }, t("Look")), + (0, import_react31.createElement)( "div", { className: "bt-create-looks" }, - (0, import_react30.createElement)(CharacterGrid, { + (0, import_react31.createElement)(CharacterGrid, { look, hasPhoto: Boolean(image), onShape: (shape) => { @@ -13688,27 +13886,27 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin setLook((current) => ({ ...current, color })); } }), - (0, import_react30.createElement)( + (0, import_react31.createElement)( "div", { className: "bt-actions bt-create-lookbar" }, - (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { + (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { setImage(null); setLook((current) => randomLook(current)); } }, t("Shuffle")), - (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), - image ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, - (0, import_react30.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { + (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), + image ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, + (0, import_react31.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { const file = event.target.files?.[0]; event.target.value = ""; upload(file); } }) ) ), - (0, import_react30.createElement)( + (0, import_react31.createElement)( "label", { className: "bt-field" }, - (0, import_react30.createElement)("span", { className: "bt-field-label" }, t("Name")), - (0, import_react30.createElement)("input", { + (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Name")), + (0, import_react31.createElement)("input", { className: "bt-input", autoFocus: !inline, value: name, @@ -13723,59 +13921,59 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin } } }), - problem ? (0, import_react30.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react30.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null + problem ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null ), - (0, import_react30.createElement)( + (0, import_react31.createElement)( "div", { className: "bt-field" }, - (0, import_react30.createElement)("span", { className: "bt-field-label" }, t("Model")), - (0, import_react30.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { + (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Model")), + (0, import_react31.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { setModel(ref); setEffort(void 0); }, onPickEffort: setEffort }), - (0, import_react30.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) + (0, import_react31.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) ), - error ? (0, import_react30.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, - (0, import_react30.createElement)( + error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, + (0, import_react31.createElement)( "div", { className: "bt-create-foot" }, // The dialog's header already has Back and Close (#230); only the inline form needs its own. - inline ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, - (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) + inline ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, + (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) ) ); - if (inline) return (0, import_react30.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); - return (0, import_react30.createElement)( + if (inline) return (0, import_react31.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); + return (0, import_react31.createElement)( "div", { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, - (0, import_react30.createElement)( + (0, import_react31.createElement)( "div", { className: "bt-create-head" }, - canGoBack ? (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react30.createElement)(ChevronLeftIcon)) : (0, import_react30.createElement)("span", { className: "bt-create-gap" }), - (0, import_react30.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), - (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react30.createElement)(CloseIcon)) + canGoBack ? (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.createElement)("span", { className: "bt-create-gap" }), + (0, import_react31.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), + (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react31.createElement)(CloseIcon)) ), - (0, import_react30.createElement)("div", { className: "bt-create-body" }, card) + (0, import_react31.createElement)("div", { className: "bt-create-body" }, card) ); } // src/client/new-chat.js -var import_react31 = require("react"); +var import_react32 = require("react"); function NewChat({ mode, roster, actions }) { - const [query, setQuery] = (0, import_react31.useState)(""); - const [picked, setPicked] = (0, import_react31.useState)([]); - const [group, setGroup] = (0, import_react31.useState)(mode === "group"); - const [compose, setCompose] = (0, import_react31.useState)(mode !== "new"); - const [admin, setAdmin] = (0, import_react31.useState)(null); - const [message, setMessage] = (0, import_react31.useState)(""); - const [busy, setBusy] = (0, import_react31.useState)(false); - const [error, setError] = (0, import_react31.useState)(""); - const [fix, setFix] = (0, import_react31.useState)(null); - const [fixNote, setFixNote] = (0, import_react31.useState)(""); - const [inviteLink, setInviteLink] = (0, import_react31.useState)(""); - const [cursor, setCursor] = (0, import_react31.useState)(0); - const [draft, setDraft] = (0, import_react31.useState)(mode === "create" ? { name: "", fromList: false } : null); - const inputRef = (0, import_react31.useRef)(null); + const [query, setQuery] = (0, import_react32.useState)(""); + const [picked, setPicked] = (0, import_react32.useState)([]); + const [group, setGroup] = (0, import_react32.useState)(mode === "group"); + const [compose, setCompose] = (0, import_react32.useState)(mode !== "new"); + const [admin, setAdmin] = (0, import_react32.useState)(null); + const [message, setMessage] = (0, import_react32.useState)(""); + const [busy, setBusy] = (0, import_react32.useState)(false); + const [error, setError] = (0, import_react32.useState)(""); + const [fix, setFix] = (0, import_react32.useState)(null); + const [fixNote, setFixNote] = (0, import_react32.useState)(""); + const [inviteLink, setInviteLink] = (0, import_react32.useState)(""); + const [cursor, setCursor] = (0, import_react32.useState)(0); + const [draft, setDraft] = (0, import_react32.useState)(mode === "create" ? { name: "", fromList: false } : null); + const inputRef = (0, import_react32.useRef)(null); const close = () => actions.closeOverlay(); useEscape(close); const paneLeft = usePaneLeft(); @@ -13906,14 +14104,14 @@ function NewChat({ mode, roster, actions }) { const options = []; if (!group && picked.length === 0 && mode !== "search" && wanted === "") { if (menuOnly) { - options.push({ key: "chat", icon: (0, import_react31.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { + options.push({ key: "chat", icon: (0, import_react32.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { setCompose(true); setCursor(0); setTimeout(() => inputRef.current?.focus()); } }); } - options.push({ key: "new", icon: (0, import_react31.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); - options.push({ key: "group", icon: (0, import_react31.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { + options.push({ key: "new", icon: (0, import_react32.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); + options.push({ key: "group", icon: (0, import_react32.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { setGroup(true); setCompose(true); setCursor(0); @@ -13922,7 +14120,7 @@ function NewChat({ mode, roster, actions }) { for (const action of newChatActionRows({ mode, group, picked, query, roster })) { options.push({ key: action.key, - icon: action.icon ? (0, import_react31.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react31.createElement)(PeopleIcon) }) : (0, import_react31.createElement)(PeopleIcon), + icon: action.icon ? (0, import_react32.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react32.createElement)(PeopleIcon) }) : (0, import_react32.createElement)(PeopleIcon), label: t(action.label), hint: action.hint ? t(action.hint) : void 0, run: () => { @@ -13934,7 +14132,7 @@ function NewChat({ mode, roster, actions }) { }); } if ((roster.agents ?? []).length === 0) { - options.push({ key: "agent", icon: (0, import_react31.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { + options.push({ key: "agent", icon: (0, import_react32.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { await actions.addAgent(); close(); }) }); @@ -13945,19 +14143,19 @@ function NewChat({ mode, roster, actions }) { options.push({ key: bot.id, bot, label: pickerBotLabel(bot), hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); } } - if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react31.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); + if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react32.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); const active = Math.min(cursor, Math.max(options.length - 1, 0)); const listed = options.map((option) => option.key).join("\n"); const glide = useGlide(`${active} ${listed}`, { axis: "y", ...QUICK_EDGES }); useFlip(glide.box, listed, "[data-flip]"); - const drop = (0, import_react31.useRef)(null); + const drop = (0, import_react32.useRef)(null); useMorph(drop, listed); if (draft) { - return (0, import_react31.createElement)( + return (0, import_react32.createElement)( "div", { className: "bt-pane", style: { left: paneLeft } }, - (0, import_react31.createElement)(CreateBot, { + (0, import_react32.createElement)(CreateBot, { roster, actions, start: draft, @@ -13977,7 +14175,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; - return (0, import_react31.createElement)( + return (0, import_react32.createElement)( "div", { className: "bt-pane", @@ -13987,31 +14185,31 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); if (event.target === event.currentTarget) close(); } : void 0 }, - (0, import_react31.createElement)( + (0, import_react32.createElement)( "div", { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, // The popover needs no title row: Escape or a press outside closes it. - menuOnly ? null : (0, import_react31.createElement)( + menuOnly ? null : (0, import_react32.createElement)( "div", { className: "bt-to" }, - (0, import_react31.createElement)("span", { className: "bt-to-label" }, t("To:")), - adminBot ? (0, import_react31.createElement)( + (0, import_react32.createElement)("span", { className: "bt-to-label" }, t("To:")), + adminBot ? (0, import_react32.createElement)( "span", { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, - (0, import_react31.createElement)(BotMark, { bot: adminBot, size: 16 }), + (0, import_react32.createElement)(BotMark, { bot: adminBot, size: 16 }), adminBot.name, - (0, import_react31.createElement)(AdminStar), - (0, import_react31.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") + (0, import_react32.createElement)(AdminStar), + (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") ) : null, - choosingAdmin ? (0, import_react31.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react31.createElement)(AdminStar)) : null, - pickedBots.map((bot) => (0, import_react31.createElement)( + choosingAdmin ? (0, import_react32.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react32.createElement)(AdminStar)) : null, + pickedBots.map((bot) => (0, import_react32.createElement)( "span", { key: bot.id, className: "bt-token" }, - (0, import_react31.createElement)(BotMark, { bot, size: 16 }), + (0, import_react32.createElement)(BotMark, { bot, size: 16 }), bot.name, - (0, import_react31.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") + (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") )), - (0, import_react31.createElement)("input", { + (0, import_react32.createElement)("input", { ref: inputRef, autoFocus: true, value: query, @@ -14035,20 +14233,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } else if (event.key === "Backspace" && query === "") unpick(); } }), - group && admin !== null ? (0, import_react31.createElement)("button", { + group && admin !== null ? (0, import_react32.createElement)("button", { type: "button", className: "bt-send bt-to-create", disabled: busy || picked.length === 0, title: picked.length === 0 ? t("Add at least one more Bot") : void 0, onClick: createGroup }, t("Create group chat")) : null, - (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") + (0, import_react32.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") ), - (0, import_react31.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react31.createElement)( + (0, import_react32.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react32.createElement)( "div", { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, - (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - options.length === 0 ? (0, import_react31.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react31.createElement)( + (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + options.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react32.createElement)( "button", { key: option.key, @@ -14060,36 +14258,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); onMouseEnter: () => setCursor(index), onClick: option.run }, - option.bot ? (0, import_react31.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react31.createElement)("span", { className: "bt-option-icon" }, option.icon), + option.bot ? (0, import_react32.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react32.createElement)("span", { className: "bt-option-icon" }, option.icon), // An action row's description sits under its title and is always there (#283): a long line // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. // A Bot row's short hint stays beside the name and shows on the active row only. - option.bot || !option.hint ? (0, import_react31.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react31.createElement)("span", { className: "bt-option-copy" }, (0, import_react31.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react31.createElement)("span", { className: "bt-option-sub" }, option.hint)), - option.bot && option.hint && index === active ? (0, import_react31.createElement)("span", { className: "bt-option-hint" }, option.hint) : null + option.bot || !option.hint ? (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react32.createElement)("span", { className: "bt-option-copy" }, (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react32.createElement)("span", { className: "bt-option-sub" }, option.hint)), + option.bot && option.hint && index === active ? (0, import_react32.createElement)("span", { className: "bt-option-hint" }, option.hint) : null )) )), - menuOnly ? null : (0, import_react31.createElement)("div", { className: "bt-nc-spacer" }), - error && fix ? (0, import_react31.createElement)( + menuOnly ? null : (0, import_react32.createElement)("div", { className: "bt-nc-spacer" }), + error && fix ? (0, import_react32.createElement)( "div", { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, - (0, import_react31.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), - (0, import_react31.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ - typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react31.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { + (0, import_react32.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), + (0, import_react32.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ + typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react32.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { void copyInvite(bot); } }, t("Copy invite link for {name}", { name: bot.name })) : null, - (0, import_react31.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) + (0, import_react32.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) ])), - fixNote ? (0, import_react31.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, - inviteLink ? (0, import_react31.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null - ) : error ? (0, import_react31.createElement)("div", { className: "bt-error" }, error) : null, - menuOnly ? null : (0, import_react31.createElement)( + fixNote ? (0, import_react32.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, + inviteLink ? (0, import_react32.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null + ) : error ? (0, import_react32.createElement)("div", { className: "bt-error" }, error) : null, + menuOnly ? null : (0, import_react32.createElement)( "div", { className: "bt-nc-compose" }, - (0, import_react31.createElement)( + (0, import_react32.createElement)( "div", { className: "bt-nc-card" }, - (0, import_react31.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react31.createElement)(PlusIcon)), - (0, import_react31.createElement)("textarea", { + (0, import_react32.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react32.createElement)(PlusIcon)), + (0, import_react32.createElement)("textarea", { rows: 1, value: message, placeholder, @@ -14102,7 +14300,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } } }), - (0, import_react31.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react31.createElement)(ArrowUpIcon)) + (0, import_react32.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react32.createElement)(ArrowUpIcon)) ) ) ) @@ -14110,18 +14308,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } // src/client/exchange.js -var import_react32 = require("react"); +var import_react33 = require("react"); var SHEET_EXIT_MS = 220; function ExchangeDialog({ pair, roster, actions }) { - const [entries2, setEntries] = (0, import_react32.useState)(null); - const [leaving, setLeaving] = (0, import_react32.useState)(false); + const [entries2, setEntries] = (0, import_react33.useState)(null); + const [leaving, setLeaving] = (0, import_react33.useState)(false); const close = () => { if (leaving) return; setLeaving(true); setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS); }; useEscape(close); - (0, import_react32.useEffect)(() => { + (0, import_react33.useEffect)(() => { let stop = false; let timer; let seen = null; @@ -14145,48 +14343,48 @@ function ExchangeDialog({ pair, roster, actions }) { const right = roster.byId[pair[1]]; let lastDay = 0; const paneLeft = usePaneLeft(); - return (0, import_react32.createElement)( + return (0, import_react33.createElement)( "div", { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, - (0, import_react32.createElement)( + (0, import_react33.createElement)( "div", { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, - (0, import_react32.createElement)("div", { className: "bt-head" }, (0, import_react32.createElement)( + (0, import_react33.createElement)("div", { className: "bt-head" }, (0, import_react33.createElement)( "span", { className: "bt-pill", style: { cursor: "default" } }, - left ? (0, import_react32.createElement)(BotMark, { bot: left, size: 22 }) : null, + left ? (0, import_react33.createElement)(BotMark, { bot: left, size: 22 }) : null, left?.name ?? "?", - (0, import_react32.createElement)("span", { className: "bt-swap" }, "⇄"), - right ? (0, import_react32.createElement)(BotMark, { bot: right, size: 22 }) : null, + (0, import_react33.createElement)("span", { className: "bt-swap" }, "⇄"), + right ? (0, import_react33.createElement)(BotMark, { bot: right, size: 22 }) : null, right?.name ?? "?" )), - (0, import_react32.createElement)( + (0, import_react33.createElement)( "div", { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, - entries2 === null ? (0, import_react32.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { + entries2 === null ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; const showDay = entry.time - lastDay > 30 * 60 * 1e3; lastDay = entry.time; - return (0, import_react32.createElement)( - import_react32.Fragment, + return (0, import_react33.createElement)( + import_react33.Fragment, { key: entry.id }, - showDay ? (0, import_react32.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, - (0, import_react32.createElement)( + showDay ? (0, import_react33.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, + (0, import_react33.createElement)( "div", { className: "bt-group-msg" }, - (0, import_react32.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), - (0, import_react32.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react32.createElement)("span", { className: "bt-lead" }, (0, import_react32.createElement)(BotMark, { bot, size: 22 })) }) + (0, import_react33.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), + (0, import_react33.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react33.createElement)("span", { className: "bt-lead" }, (0, import_react33.createElement)(BotMark, { bot, size: 22 })) }) ) ); }) ), - (0, import_react32.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) + (0, import_react33.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) ) ); } // src/client/voice.js -var import_react33 = require("react"); +var import_react34 = require("react"); var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); var VOICE_LABELS = { listening: "Listening", @@ -14198,8 +14396,8 @@ var VOICE_LABELS = { }; var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; function useMicLevel(enabled) { - const [level, setLevel] = (0, import_react33.useState)(0); - (0, import_react33.useEffect)(() => { + const [level, setLevel] = (0, import_react34.useState)(0); + (0, import_react34.useEffect)(() => { if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; let stream = null; let audio = null; @@ -14249,18 +14447,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); - const [speech] = (0, import_react33.useState)(() => actions.speech()); - const [phase, setPhase] = (0, import_react33.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); - const [catalog, setCatalog] = (0, import_react33.useState)(null); - const [heard, setHeard] = (0, import_react33.useState)(""); - const [said, setSaid] = (0, import_react33.useState)(""); - const [note, setNote] = (0, import_react33.useState)(""); - const [serviceLevel, setServiceLevel] = (0, import_react33.useState)(0); - const [cycle, setCycle] = (0, import_react33.useState)(0); - const waiting = (0, import_react33.useRef)(null); - const reply = (0, import_react33.useRef)(lastReply); + const [speech] = (0, import_react34.useState)(() => actions.speech()); + const [phase, setPhase] = (0, import_react34.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); + const [catalog, setCatalog] = (0, import_react34.useState)(null); + const [heard, setHeard] = (0, import_react34.useState)(""); + const [said, setSaid] = (0, import_react34.useState)(""); + const [note, setNote] = (0, import_react34.useState)(""); + const [serviceLevel, setServiceLevel] = (0, import_react34.useState)(0); + const [cycle, setCycle] = (0, import_react34.useState)(0); + const waiting = (0, import_react34.useRef)(null); + const reply = (0, import_react34.useRef)(lastReply); reply.current = lastReply; - const turn = (0, import_react33.useRef)(0); + const turn = (0, import_react34.useRef)(0); const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; @@ -14280,12 +14478,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } dictation.stop(); submit(text); }); - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); }, [note]); - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { @@ -14300,16 +14498,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } }; }, [speech]); const listen = phase === "listening"; - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (speech || !listen || dictation.listening) return void 0; const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { } }), 250); return () => clearTimeout(timer); }, [listen, dictation.listening]); - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (phase !== "listening") dictation.stop(); }, [phase]); - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; @@ -14371,7 +14569,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); - (0, import_react33.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (phase !== "thinking" || running || waiting.current === null) return; const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; if (count === waiting.current || !text.trim()) return; @@ -14391,7 +14589,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); - (0, import_react33.useEffect)(() => () => window.speechSynthesis?.cancel(), []); + (0, import_react34.useEffect)(() => () => window.speechSynthesis?.cancel(), []); const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; @@ -14408,44 +14606,44 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; - return (0, import_react33.createElement)( + return (0, import_react34.createElement)( "div", { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, - (0, import_react33.createElement)( + (0, import_react34.createElement)( "div", { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, - (0, import_react33.createElement)( + (0, import_react34.createElement)( "div", { className: "bt-voice-orb", style: { "--bt-level": level } }, - (0, import_react33.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), - (0, import_react33.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), - bot ? (0, import_react33.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react33.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null + (0, import_react34.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), + (0, import_react34.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), + bot ? (0, import_react34.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null ), - (0, import_react33.createElement)("div", { className: "bt-voice-name" }, name), - (0, import_react33.createElement)( + (0, import_react34.createElement)("div", { className: "bt-voice-name" }, name), + (0, import_react34.createElement)( "div", { className: "bt-voice-status", role: "status" }, - phase === "thinking" ? (0, import_react33.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react33.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" + phase === "thinking" ? (0, import_react34.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react34.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" ), - (0, import_react33.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), - openSetup ? (0, import_react33.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { + (0, import_react34.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), + openSetup ? (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { close(); openSetup(); } }, t("Set up voice input")) : null, - shownNote ? (0, import_react33.createElement)("div", { className: "bt-voice-note" }, shownNote) : null + shownNote ? (0, import_react34.createElement)("div", { className: "bt-voice-note" }, shownNote) : null ), - (0, import_react33.createElement)( + (0, import_react34.createElement)( "div", { className: "bt-voice-bar" }, - (0, import_react33.createElement)("button", { + (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-btn", "aria-pressed": phase === "paused", disabled: blocked, "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), onClick: togglePause - }, (0, import_react33.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react33.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), - (0, import_react33.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react33.createElement)(CloseIcon)) + }, (0, import_react34.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react34.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), + (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react34.createElement)(CloseIcon)) ) ); } @@ -14583,7 +14781,7 @@ function percent(part, whole) { } // src/client/usage-page.js -var import_react34 = require("react"); +var import_react35 = require("react"); var HEAT_WEEKS = 53; var TOP_SERIES = 7; var RING_R = 42; @@ -14630,9 +14828,9 @@ function titleOf(range, bucket) { return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { - const [shown, setShown] = (0, import_react34.useState)(0); - const from = (0, import_react34.useRef)(0); - (0, import_react34.useEffect)(() => { + const [shown, setShown] = (0, import_react35.useState)(0); + const from = (0, import_react35.useRef)(0); + (0, import_react35.useEffect)(() => { if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); @@ -14652,9 +14850,9 @@ function useTween(value) { }, [value]); return shown; } -var glyphBox = (size, icon3) => (0, import_react34.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react34.createElement)(icon3)); +var glyphBox = (size, icon3) => (0, import_react35.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react35.createElement)(icon3)); function ModelGlyph({ name, paint, size }) { - return (0, import_react34.createElement)("span", { + return (0, import_react35.createElement)("span", { className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } @@ -14671,7 +14869,7 @@ function ownerFace(owner, roster) { sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], - glyph: (size) => (0, import_react34.createElement)(BotAvatar, { bot, size, badge: false, live: false }) + glyph: (size) => (0, import_react35.createElement)(BotAvatar, { bot, size, badge: false, live: false }) }; } if (owner.kind === "room") { @@ -14681,7 +14879,7 @@ function ownerFace(owner, roster) { name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", - glyph: (size) => room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) + glyph: (size) => room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) }; } if (owner.kind === "agent") { @@ -14703,30 +14901,30 @@ function Tabs({ label, value, options, onChange, boxRef }) { const names = options.map(([, text]) => text).join("\n"); const glide = useGlide(`${value} ${names}`, { box: boxRef }); - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "div", { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, - (0, import_react34.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - options.map(([id, text, icon3]) => (0, import_react34.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react34.createElement)(icon3) : null, text)) + (0, import_react35.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + options.map(([id, text, icon3]) => (0, import_react35.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react35.createElement)(icon3) : null, text)) ); } function Stats({ range, total, previous }) { const shown = useTween(total.total); const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; - const cell = (label, value) => (0, import_react34.createElement)("div", { className: "bt-us-stat" }, (0, import_react34.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react34.createElement)("span", { className: "bt-us-stat-value" }, value)); - return (0, import_react34.createElement)( + const cell = (label, value) => (0, import_react35.createElement)("div", { className: "bt-us-stat" }, (0, import_react35.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react35.createElement)("span", { className: "bt-us-stat-value" }, value)); + return (0, import_react35.createElement)( "section", { className: "bt-us-stats", style: { "--i": 0 } }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-stat bt-us-stat-main" }, - (0, import_react34.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), - (0, import_react34.createElement)( + (0, import_react35.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), + (0, import_react35.createElement)( "span", { className: "bt-us-stat-value" }, compact(shown), - (0, import_react34.createElement)("small", null, t("tokens")), - change === null ? null : (0, import_react34.createElement)( + (0, import_react35.createElement)("small", null, t("tokens")), + change === null ? null : (0, import_react35.createElement)( "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` @@ -14744,83 +14942,83 @@ function niceTop(max) { return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { - const [hover, setHover] = (0, import_react34.useState)(null); + const [hover, setHover] = (0, import_react35.useState)(null); const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); const count = columns.length; const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); const tipOf = new Map(tipSeries.map((entry) => [entry.key, entry])); const tip = hover === null ? null : columns[hover]; const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "section", { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-card-head" }, - (0, import_react34.createElement)("h3", null, t("Tokens over time"), (0, import_react34.createElement)("span", null, unitLabel(range.unit))), - onBy ? (0, import_react34.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null + (0, import_react35.createElement)("h3", null, t("Tokens over time"), (0, import_react35.createElement)("span", null, unitLabel(range.unit))), + onBy ? (0, import_react35.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null ), - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-chart-body" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, - [1, 0.5].map((level) => (0, import_react34.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react34.createElement)("span", null, compact(top * level)))), - (0, import_react34.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react34.createElement)("span", null, "0")), - (0, import_react34.createElement)( + [1, 0.5].map((level) => (0, import_react35.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react35.createElement)("span", null, compact(top * level)))), + (0, import_react35.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react35.createElement)("span", null, "0")), + (0, import_react35.createElement)( "div", { className: "bt-us-cols" }, - columns.map((column, at) => (0, import_react34.createElement)("div", { + columns.map((column, at) => (0, import_react35.createElement)("div", { key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, className: "bt-us-col", "data-on": hover === at ? "" : void 0, onMouseEnter: () => setHover(at) - }, column.total > 0 ? (0, import_react34.createElement)( + }, column.total > 0 ? (0, import_react35.createElement)( "div", { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, - column.parts.map(([key, value]) => value > 0 ? (0, import_react34.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) - ) : (0, import_react34.createElement)("div", { className: "bt-us-stub" }))) + column.parts.map(([key, value]) => value > 0 ? (0, import_react35.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) + ) : (0, import_react35.createElement)("div", { className: "bt-us-stub" }))) ), - tip ? (0, import_react34.createElement)( + tip ? (0, import_react35.createElement)( "div", { className: "bt-us-tip", style: tipSide }, - (0, import_react34.createElement)("div", { className: "bt-us-tip-title" }, tip.title), - (0, import_react34.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), - tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-tip-title" }, tip.title), + (0, import_react35.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), + tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react35.createElement)( "div", { key, className: "bt-us-tip-row" }, - (0, import_react34.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), - (0, import_react34.createElement)("span", null, tipOf.get(key)?.name), - (0, import_react34.createElement)("b", null, compact(value)) + (0, import_react35.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), + (0, import_react35.createElement)("span", null, tipOf.get(key)?.name), + (0, import_react35.createElement)("b", null, compact(value)) )) ) : null ), - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-ticks", "aria-hidden": true }, // A preset counts its labels back from now; a picked span forward from its first day. - columns.map((column, at) => (0, import_react34.createElement)( + columns.map((column, at) => (0, import_react35.createElement)( "span", { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" )) ) ), - series.length > 1 || series[0]?.pick ? (0, import_react34.createElement)( + series.length > 1 || series[0]?.pick ? (0, import_react35.createElement)( "div", { className: "bt-us-legend" }, - series.map((entry) => (0, import_react34.createElement)(entry.pick ? "button" : "span", { + series.map((entry) => (0, import_react35.createElement)(entry.pick ? "button" : "span", { key: entry.key, className: "bt-us-key", ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} - }, (0, import_react34.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) + }, (0, import_react35.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) ) : null ); } function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick }) { const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( onClick ? "button" : "div", { className: "bt-us-row", @@ -14829,69 +15027,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, - glyph ? (0, import_react34.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, - (0, import_react34.createElement)( + glyph ? (0, import_react35.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, + (0, import_react35.createElement)( "span", { className: "bt-us-row-body" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "span", { className: "bt-us-row-line" }, - (0, import_react34.createElement)("span", { className: "bt-us-row-name" }, name), - sub ? (0, import_react34.createElement)("span", { className: "bt-us-row-sub" }, sub) : null + (0, import_react35.createElement)("span", { className: "bt-us-row-name" }, name), + sub ? (0, import_react35.createElement)("span", { className: "bt-us-row-sub" }, sub) : null ), - (0, import_react34.createElement)("span", { className: "bt-us-track" }, (0, import_react34.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) + (0, import_react35.createElement)("span", { className: "bt-us-track" }, (0, import_react35.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) ), - (0, import_react34.createElement)("span", { className: "bt-us-row-num" }, (0, import_react34.createElement)("b", null, compact(sum.total)), (0, import_react34.createElement)("small", null, `${percent(sum.total, whole)}%`)) + (0, import_react35.createElement)("span", { className: "bt-us-row-num" }, (0, import_react35.createElement)("b", null, compact(sum.total)), (0, import_react35.createElement)("small", null, `${percent(sum.total, whole)}%`)) ); } function ListCard({ title, count, index, children }) { - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "section", { className: "bt-us-card", style: { "--i": index } }, - (0, import_react34.createElement)("div", { className: "bt-us-card-head" }, (0, import_react34.createElement)("h3", null, title, count === void 0 ? null : (0, import_react34.createElement)("span", null, count))), - (0, import_react34.createElement)("div", { className: "bt-us-rows" }, children) + (0, import_react35.createElement)("div", { className: "bt-us-card-head" }, (0, import_react35.createElement)("h3", null, title, count === void 0 ? null : (0, import_react35.createElement)("span", null, count))), + (0, import_react35.createElement)("div", { className: "bt-us-rows" }, children) ); } function Profile({ face, share, range }) { const arc = Math.max(0, Math.min(1, share)) * RING2; - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "section", { className: "bt-us-profile" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "span", { className: "bt-us-ring" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "svg", { viewBox: "0 0 96 96", "aria-hidden": true }, - face.stops ? (0, import_react34.createElement)("defs", null, (0, import_react34.createElement)( + face.stops ? (0, import_react35.createElement)("defs", null, (0, import_react35.createElement)( "linearGradient", { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, - face.stops.map(([offset, color]) => (0, import_react34.createElement)("stop", { key: offset, offset, stopColor: color })) + face.stops.map(([offset, color]) => (0, import_react35.createElement)("stop", { key: offset, offset, stopColor: color })) )) : null, - (0, import_react34.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), - arc > 0 ? (0, import_react34.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null + (0, import_react35.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), + arc > 0 ? (0, import_react35.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null ), - (0, import_react34.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) + (0, import_react35.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) ), - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-profile-copy" }, - (0, import_react34.createElement)("h3", null, face.name), - face.sub ? (0, import_react34.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, - (0, import_react34.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) + (0, import_react35.createElement)("h3", null, face.name), + face.sub ? (0, import_react35.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, + (0, import_react35.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) ) ); } var monthOf = (ms) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth(), 1); var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { - const box = (0, import_react34.useRef)(null); + const box = (0, import_react35.useRef)(null); const todayMs = hourMs(today); const thisMonth = monthOf(todayMs); - const [right, setRight] = (0, import_react34.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); - const [anchor, setAnchor] = (0, import_react34.useState)(null); - const [hover, setHover] = (0, import_react34.useState)(null); - (0, import_react34.useEffect)(() => { + const [right, setRight] = (0, import_react35.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); + const [anchor, setAnchor] = (0, import_react35.useState)(null); + const [hover, setHover] = (0, import_react35.useState)(null); + (0, import_react35.useEffect)(() => { const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; @@ -14917,17 +15115,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { const [from, to] = [anchor, key].sort(); onPick(from, to); }; - const weekdays = (0, import_react34.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); + const weekdays = (0, import_react35.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); const month = (start, side) => { const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); const lead = (new Date(start).getUTCDay() + 6) % 7; - const cells = Array.from({ length: lead }, (_, at) => (0, import_react34.createElement)("span", { key: `b${at}` })); + const cells = Array.from({ length: lead }, (_, at) => (0, import_react35.createElement)("span", { key: `b${at}` })); for (let day = 1; day <= length; day += 1) { const ms = start + (day - 1) * DAY_MS; const key = dayKey(ms); const weekday = (lead + day - 1) % 7; const flag = (on) => on ? "" : void 0; - cells.push((0, import_react34.createElement)("button", { + cells.push((0, import_react35.createElement)("button", { key, type: "button", className: "bt-us-day", @@ -14943,15 +15141,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) - }, (0, import_react34.createElement)("b", null, day))); + }, (0, import_react35.createElement)("b", null, day))); } - const nav = side === "left" ? (0, import_react34.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react34.createElement)(ChevronLeftIcon)) : (0, import_react34.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react34.createElement)(ChevronRightIcon)); - return (0, import_react34.createElement)( + const nav = side === "left" ? (0, import_react35.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react35.createElement)(ChevronLeftIcon)) : (0, import_react35.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react35.createElement)(ChevronRightIcon)); + return (0, import_react35.createElement)( "div", { key: start, className: "bt-us-month" }, - (0, import_react34.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react34.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), - (0, import_react34.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react34.createElement)("span", { key: at }, name))), - (0, import_react34.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) + (0, import_react35.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react35.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), + (0, import_react35.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react35.createElement)("span", { key: at }, name))), + (0, import_react35.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) ); }; const shortcuts = [ @@ -14960,30 +15158,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "div", { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-pick-quick" }, - shortcuts.map(([id, label, from, to]) => (0, import_react34.createElement)("button", { + shortcuts.map(([id, label, from, to]) => (0, import_react35.createElement)("button", { key: id, type: "button", "aria-pressed": span?.from === from && span?.to === to, onClick: () => onPick(from, to) }, label)) ), - (0, import_react34.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), - (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), + (0, import_react35.createElement)( "div", { className: "bt-us-pick-foot", role: "status" }, - (0, import_react34.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), - low === null ? null : (0, import_react34.createElement)("span", null, (0, import_react34.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) + (0, import_react35.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), + low === null ? null : (0, import_react35.createElement)("span", null, (0, import_react35.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) ) ); } function Heat({ view, keep, paint, index }) { - const [hover, setHover] = (0, import_react34.useState)(null); + const [hover, setHover] = (0, import_react35.useState)(null); const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); @@ -14999,13 +15197,13 @@ function Heat({ view, keep, paint, index }) { for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { - months.push((0, import_react34.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); + months.push((0, import_react35.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); } for (let day = 0; day < 7; day += 1) { const ms = monday + day * DAY_MS; const value = daily.get(dayKey(ms)) ?? 0; const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; - cells.push((0, import_react34.createElement)("span", { + cells.push((0, import_react35.createElement)("span", { key: ms, className: "bt-us-cell", style: { "--w": week }, @@ -15013,7 +15211,7 @@ function Heat({ view, keep, paint, index }) { "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) - }, level > 0 ? (0, import_react34.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); + }, level > 0 ? (0, import_react35.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); } } const tipSide = hover ? { @@ -15022,61 +15220,61 @@ function Heat({ view, keep, paint, index }) { } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "section", { className: "bt-us-card bt-us-heat", style: { "--i": index } }, - (0, import_react34.createElement)("div", { className: "bt-us-card-head" }, (0, import_react34.createElement)("h3", null, t("Activity")), (0, import_react34.createElement)("span", { className: "bt-us-head-note" }, summary)), - (0, import_react34.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), - (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-card-head" }, (0, import_react35.createElement)("h3", null, t("Activity")), (0, import_react35.createElement)("span", { className: "bt-us-head-note" }, summary)), + (0, import_react35.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), + (0, import_react35.createElement)( "div", { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, - (0, import_react34.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), - hover ? (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), + hover ? (0, import_react35.createElement)( "div", { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, - (0, import_react34.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), - (0, import_react34.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) + (0, import_react35.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), + (0, import_react35.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) ) : null ), - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-heat-scale", "aria-hidden": true }, t("Light"), - levels.map((level) => (0, import_react34.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react34.createElement)("i", { style: { background: soften(paint, level) } }))), + levels.map((level) => (0, import_react35.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react35.createElement)("i", { style: { background: soften(paint, level) } }))), t("Heavy") ) ); } function Skeleton2() { - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "div", { className: "bt-us", "aria-busy": true }, - (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), - (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), + (0, import_react35.createElement)( "div", { className: "bt-us-sheet bt-us-skel-sheet" }, - (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), - (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), - (0, import_react34.createElement)( + (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), + (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), + (0, import_react35.createElement)( "div", { className: "bt-us-pair" }, - (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), - (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) + (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), + (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) ) ) ); } var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null; function UsagePage({ roster, actions, focusId }) { - const [view, setView] = (0, import_react34.useState)(null); - const [error, setError] = (0, import_react34.useState)(""); - const [rangeId, setRangeId] = (0, import_react34.useState)("7d"); - const [span, setSpan] = (0, import_react34.useState)(null); - const [picking, setPicking] = (0, import_react34.useState)(false); - const [by, setBy] = (0, import_react34.useState)("bot"); - const tabs = (0, import_react34.useRef)(null); + const [view, setView] = (0, import_react35.useState)(null); + const [error, setError] = (0, import_react35.useState)(""); + const [rangeId, setRangeId] = (0, import_react35.useState)("7d"); + const [span, setSpan] = (0, import_react35.useState)(null); + const [picking, setPicking] = (0, import_react35.useState)(false); + const [by, setBy] = (0, import_react35.useState)("bot"); + const tabs = (0, import_react35.useRef)(null); const focus = focusOf(focusId); - (0, import_react34.useEffect)(() => { + (0, import_react35.useEffect)(() => { let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { @@ -15086,7 +15284,7 @@ function UsagePage({ roster, actions, focusId }) { const value = await actions.usage(); await resized; if (stopped) return; - (0, import_react34.startTransition)(() => setView(value)); + (0, import_react35.startTransition)(() => setView(value)); setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { @@ -15103,7 +15301,7 @@ function UsagePage({ roster, actions, focusId }) { }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); - const shaped = (0, import_react34.useMemo)(() => { + const shaped = (0, import_react35.useMemo)(() => { if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); @@ -15112,7 +15310,7 @@ function UsagePage({ roster, actions, focusId }) { const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; - return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react34.createElement)(ModelGlyph, { name, paint, size }) }; + return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react35.createElement)(ModelGlyph, { name, paint, size }) }; }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; @@ -15160,23 +15358,23 @@ function UsagePage({ roster, actions, focusId }) { }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); - const root = (0, import_react34.useRef)(null); - (0, import_react34.useEffect)(() => { + const root = (0, import_react35.useRef)(null); + (0, import_react35.useEffect)(() => { root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); }, [focusId]); if (!view) { - return error ? (0, import_react34.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react34.createElement)(Skeleton2); + return error ? (0, import_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react35.createElement)(Skeleton2); } if (view.rows.length === 0) { - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "div", { className: "bt-us" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-empty" }, - (0, import_react34.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), - (0, import_react34.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), - (0, import_react34.createElement)("span", null, t("Token usage shows up here after a model is called.")) + (0, import_react35.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), + (0, import_react35.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), + (0, import_react35.createElement)("span", null, t("Token usage shows up here after a model is called.")) ) ); } @@ -15201,7 +15399,7 @@ function UsagePage({ roster, actions, focusId }) { ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; - const ownerRow = ([at, sum], index, total = whole) => (0, import_react34.createElement)(Row2, { + const ownerRow = ([at, sum], index, total = whole) => (0, import_react35.createElement)(Row2, { key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), @@ -15213,7 +15411,7 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); - const modelRow = ([at, sum], index) => (0, import_react34.createElement)(Row2, { + const modelRow = ([at, sum], index) => (0, import_react35.createElement)(Row2, { key: `m${at}`, kind: "model", glyph: models[at].glyph(32), @@ -15225,30 +15423,30 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); - const nothing = (0, import_react34.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); + const nothing = (0, import_react35.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); let body; if (face) { const kinds = ranked(part.kinds); const usedModels = ranked(part.models); const owners = ranked(part.owners); body = [ - (0, import_react34.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), - (0, import_react34.createElement)( + (0, import_react35.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), + (0, import_react35.createElement)( "div", { key: "sheet", className: "bt-us-sheet" }, - (0, import_react34.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), - (0, import_react34.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), - (0, import_react34.createElement)( + (0, import_react35.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), + (0, import_react35.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), + (0, import_react35.createElement)( "div", { key: "pair", className: "bt-us-pair" }, - ownerAt >= 0 ? (0, import_react34.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react34.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), - (0, import_react34.createElement)( + ownerAt >= 0 ? (0, import_react35.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react35.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), + (0, import_react35.createElement)( ListCard, { title: t("What for"), index: 3 }, - kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react34.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing + kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react35.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing ) ), - (0, import_react34.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) + (0, import_react35.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) ) ]; } else { @@ -15257,39 +15455,39 @@ function UsagePage({ roster, actions, focusId }) { const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { key: "sheet", className: "bt-us-sheet" }, - (0, import_react34.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), - (0, import_react34.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), - (0, import_react34.createElement)( + (0, import_react35.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), + (0, import_react35.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), + (0, import_react35.createElement)( "div", { key: "pair", className: "bt-us-pair" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( ListCard, { title: t("Bots"), count: bots.length, index: 2 }, owners.length > 0 ? null : nothing, bots.map((entry, index) => ownerRow(entry, index)), - bots.length > 0 && rest.length > 0 ? (0, import_react34.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, + bots.length > 0 && rest.length > 0 ? (0, import_react35.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, rest.map((entry, index) => ownerRow(entry, bots.length + index)) ), - (0, import_react34.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) + (0, import_react35.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) ), - (0, import_react34.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) + (0, import_react35.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) ) ]; } - return (0, import_react34.createElement)( + return (0, import_react35.createElement)( "div", { ref: root, className: "bt-us", key: focusId ?? "all" }, - (0, import_react34.createElement)( + (0, import_react35.createElement)( "div", { className: "bt-us-top" }, - (0, import_react34.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), - view.scanning ? (0, import_react34.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, - picking ? (0, import_react34.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null + (0, import_react35.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), + view.scanning ? (0, import_react35.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, + picking ? (0, import_react35.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null ), - error ? (0, import_react34.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, + error ? (0, import_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, body ); } @@ -15446,7 +15644,7 @@ button.bt-us-row:hover{background:var(--bt-hover)} `; // src/client/connectors.js -var import_react35 = require("react"); +var import_react36 = require("react"); var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; var APPS = [ @@ -15479,10 +15677,10 @@ var APPS = [ ["youtube", "YouTube"] ]; var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; -var GitHubMark = () => (0, import_react35.createElement)( +var GitHubMark = () => (0, import_react36.createElement)( "svg", { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, - (0, import_react35.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) + (0, import_react36.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) ); var MARKS = { github: GitHubMark, composio: ConnectorIcon }; var ABOUT = { @@ -15499,20 +15697,20 @@ function statusText2(connector) { var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; function AppChip({ name, status, onRemove }) { const label = APP_STATUS[status]?.(); - return (0, import_react35.createElement)( + return (0, import_react36.createElement)( "button", { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, name, - label ? (0, import_react35.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null + label ? (0, import_react36.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null ); } function AppPicker({ apps, onChange, search, statuses = {} }) { - const [query, setQuery] = (0, import_react35.useState)(""); - const [found, setFound] = (0, import_react35.useState)(null); - const [failed, setFailed] = (0, import_react35.useState)(null); - const names = (0, import_react35.useRef)(new Map(APPS)); - const asked = (0, import_react35.useRef)(0); - (0, import_react35.useEffect)(() => { + const [query, setQuery] = (0, import_react36.useState)(""); + const [found, setFound] = (0, import_react36.useState)(null); + const [failed, setFailed] = (0, import_react36.useState)(null); + const names = (0, import_react36.useRef)(new Map(APPS)); + const asked = (0, import_react36.useRef)(0); + (0, import_react36.useEffect)(() => { const text = query.trim(); if (text === "" || search === void 0) { setFound(null); @@ -15539,13 +15737,13 @@ function AppPicker({ apps, onChange, search, statuses = {} }) { const needle = query.trim().toLowerCase(); const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); const results = (found ?? local).filter((app) => !picked.has(app.slug)); - return (0, import_react35.createElement)( + return (0, import_react36.createElement)( "div", { className: "bt-connector-apps" }, - apps.length > 0 ? (0, import_react35.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react35.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, - (0, import_react35.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), - failed ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, - (0, import_react35.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react35.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { + apps.length > 0 ? (0, import_react36.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react36.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, + (0, import_react36.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), + failed ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, + (0, import_react36.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react36.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { names.current.set(app.slug, app.name); onChange([...apps, app.slug]); } }, app.name))) @@ -15553,21 +15751,21 @@ function AppPicker({ apps, onChange, search, statuses = {} }) { } function AppStatusList({ apps, statuses }) { if (apps.length === 0 || Object.keys(statuses).length === 0) return null; - return (0, import_react35.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react35.createElement)( + return (0, import_react36.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react36.createElement)( "li", { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, - (0, import_react35.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), - (0, import_react35.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") + (0, import_react36.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), + (0, import_react36.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") ))); } function ConnectorRow({ connector, roster, actions }) { - const [form, setForm] = (0, import_react35.useState)(false); - const [bots, setBots] = (0, import_react35.useState)(false); - const [token, setToken] = (0, import_react35.useState)(""); - const [apps, setApps] = (0, import_react35.useState)(() => connector.apps ?? []); - const [picking, setPicking] = (0, import_react35.useState)(false); - const [busy, setBusy] = (0, import_react35.useState)(false); - const [error, setError] = (0, import_react35.useState)(null); + const [form, setForm] = (0, import_react36.useState)(false); + const [bots, setBots] = (0, import_react36.useState)(false); + const [token, setToken] = (0, import_react36.useState)(""); + const [apps, setApps] = (0, import_react36.useState)(() => connector.apps ?? []); + const [picking, setPicking] = (0, import_react36.useState)(false); + const [busy, setBusy] = (0, import_react36.useState)(false); + const [error, setError] = (0, import_react36.useState)(null); const run = (task, after) => { setBusy(true); setError(null); @@ -15576,9 +15774,9 @@ function ConnectorRow({ connector, roster, actions }) { }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; const gateway = connector.id === "composio"; - const [statuses, setStatuses] = (0, import_react35.useState)({}); + const [statuses, setStatuses] = (0, import_react36.useState)({}); const appsKey = (connector.apps ?? []).join(","); - (0, import_react35.useEffect)(() => { + (0, import_react36.useEffect)(() => { if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { setStatuses({}); return void 0; @@ -15593,7 +15791,7 @@ function ConnectorRow({ connector, roster, actions }) { live = false; }; }, [gateway, connector.state, appsKey, picking]); - const search = (0, import_react35.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); + const search = (0, import_react36.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); const connect = () => { if (busy || token.trim() === "") return; if (gateway && apps.length === 0) { @@ -15615,88 +15813,88 @@ function ConnectorRow({ connector, roster, actions }) { }; const on = connector.state !== "off"; const Mark = MARKS[connector.id]; - const row = (0, import_react35.useRef)(null); + const row = (0, import_react36.useRef)(null); useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`); - return (0, import_react35.createElement)( + return (0, import_react36.createElement)( "div", { ref: row, className: "bt-connector", "data-state": connector.state }, - (0, import_react35.createElement)( + (0, import_react36.createElement)( "div", { className: "bt-connector-head" }, - (0, import_react35.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react35.createElement)(Mark) : null), - (0, import_react35.createElement)( + (0, import_react36.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react36.createElement)(Mark) : null), + (0, import_react36.createElement)( "span", { className: "bt-connector-text" }, - (0, import_react35.createElement)("span", { className: "bt-connector-name" }, connector.name), - (0, import_react35.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) + (0, import_react36.createElement)("span", { className: "bt-connector-name" }, connector.name), + (0, import_react36.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) ), - (0, import_react35.createElement)( + (0, import_react36.createElement)( "span", { className: "bt-connector-actions" }, - connector.state === "error" ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, - on && gateway && !picking ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + connector.state === "error" ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, + on && gateway && !picking ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { setApps(connector.apps ?? []); setPicking(true); } }, t("Choose apps")) : null, - on ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, - on ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) + on ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, + on ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) ) ), - on && gateway && !picking ? (0, import_react35.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, - on ? (0, import_react35.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, - on && bots ? (0, import_react35.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, - !on && form ? (0, import_react35.createElement)( + on && gateway && !picking ? (0, import_react36.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, + on ? (0, import_react36.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, + on && bots ? (0, import_react36.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, + !on && form ? (0, import_react36.createElement)( "div", { className: "bt-connector-form" }, - (0, import_react35.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react35.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), - gateway ? (0, import_react35.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, - (0, import_react35.createElement)( + (0, import_react36.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react36.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react36.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react36.createElement)( "div", { className: "bt-connector-form-row" }, - (0, import_react35.createElement)("a", { className: "bt-connector-link", href: gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, gateway ? t("Get a Composio API key") : t("Create a token on GitHub")), - (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + (0, import_react36.createElement)("a", { className: "bt-connector-link", href: gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, gateway ? t("Get a Composio API key") : t("Create a token on GitHub")), + (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { setForm(false); setToken(""); setError(null); } }, t("Cancel")), - (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) + (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) ) ) : null, - on && gateway && picking ? (0, import_react35.createElement)( + on && gateway && picking ? (0, import_react36.createElement)( "div", { className: "bt-connector-form" }, - (0, import_react35.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), - (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), - (0, import_react35.createElement)( + (0, import_react36.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), + (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), + (0, import_react36.createElement)( "div", { className: "bt-connector-form-row" }, - (0, import_react35.createElement)("span", { className: "bt-connector-link" }), - (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + (0, import_react36.createElement)("span", { className: "bt-connector-link" }), + (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { setPicking(false); setError(null); } }, t("Cancel")), - (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) + (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) ) ) : null, - error ? (0, import_react35.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null + error ? (0, import_react36.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ); } function ConnectorsPage({ roster, actions }) { const list = roster?.connectors ?? []; - return (0, import_react35.createElement)( + return (0, import_react36.createElement)( "section", { className: "bt-set-section" }, - (0, import_react35.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react35.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) + (0, import_react36.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react36.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) ); } // src/client/feedback.js -var import_react36 = require("react"); -var FeedbackIcon = () => (0, import_react36.createElement)( +var import_react37 = require("react"); +var FeedbackIcon = () => (0, import_react37.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_react36.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), - (0, import_react36.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) + (0, import_react37.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), + (0, import_react37.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) ); function download(name, text) { const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); @@ -15719,16 +15917,16 @@ function entrySummary(entry) { } var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" }; function FeedbackPage({ actions, run, Section, Row: Row3 }) { - const [what, setWhat] = (0, import_react36.useState)(""); - const [report, setReport] = (0, import_react36.useState)(null); - const [preview, setPreview] = (0, import_react36.useState)(false); - const [note, setNote] = (0, import_react36.useState)(""); + const [what, setWhat] = (0, import_react37.useState)(""); + const [report, setReport] = (0, import_react37.useState)(null); + const [preview, setPreview] = (0, import_react37.useState)(false); + const [note, setNote] = (0, import_react37.useState)(""); const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { setReport(next); return next; }); const load = () => fetchReport().then(() => void 0); - (0, import_react36.useEffect)(() => { + (0, import_react37.useEffect)(() => { void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { @@ -15736,7 +15934,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); - const [issueLink, setIssueLink] = (0, import_react36.useState)(null); + const [issueLink, setIssueLink] = (0, import_react37.useState)(null); const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; @@ -15764,59 +15962,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ - (0, import_react36.createElement)( + (0, import_react37.createElement)( Section, { key: "report", title: t("Report a problem") }, - (0, import_react36.createElement)( + (0, import_react37.createElement)( "div", { className: "bt-set-row bt-set-paste bt-fb-what" }, - (0, import_react36.createElement)( + (0, import_react37.createElement)( "label", { className: "bt-set-copy", htmlFor: "bt-fb-what" }, - (0, import_react36.createElement)("span", null, t("What happened?")), - (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) + (0, import_react37.createElement)("span", null, t("What happened?")), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) ), - (0, import_react36.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) + (0, import_react37.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) ), - (0, import_react36.createElement)( + (0, import_react37.createElement)( Row3, { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, - (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), - (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) ), - preview ? (0, import_react36.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react36.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, - (0, import_react36.createElement)( + preview ? (0, import_react37.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react37.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, + (0, import_react37.createElement)( Row3, { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, - (0, import_react36.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) + (0, import_react37.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) ) ), - note ? (0, import_react36.createElement)( + note ? (0, import_react37.createElement)( "div", { key: "note", className: "bt-note bt-fb-note", role: "status" }, note, - issueLink ? [" ", (0, import_react36.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null + issueLink ? [" ", (0, import_react37.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null ) : null, - (0, import_react36.createElement)( + (0, import_react37.createElement)( Section, { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, - shown.length === 0 ? (0, import_react36.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react36.createElement)( + shown.length === 0 ? (0, import_react37.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react37.createElement)( "div", { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, - (0, import_react36.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), - (0, import_react36.createElement)( + (0, import_react37.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), + (0, import_react37.createElement)( "div", { className: "bt-set-copy" }, - (0, import_react36.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), - (0, import_react36.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) + (0, import_react37.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) ), - entry.code ? (0, import_react36.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null + entry.code ? (0, import_react37.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null )), - (0, import_react36.createElement)( + (0, import_react37.createElement)( Row3, { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, - (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), - (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) ) ) ]; @@ -15824,8 +16022,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); - const [leaving, setLeaving] = (0, import_react36.useState)(false); - (0, import_react36.useEffect)(() => { + const [leaving, setLeaving] = (0, import_react37.useState)(false); + (0, import_react37.useEffect)(() => { if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); @@ -15837,15 +16035,15 @@ function ErrorToast({ useUi, actions }) { }, [toast?.token]); if (!toast) return null; const info = toast.tone === "info"; - return (0, import_react36.createElement)( + return (0, import_react37.createElement)( "div", { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, - (0, import_react36.createElement)("span", { className: "bt-toast-text" }, toast.text), - info ? null : (0, import_react36.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { + (0, import_react37.createElement)("span", { className: "bt-toast-text" }, toast.text), + info ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { actions.dismissToast(toast.token); actions.openSettings("feedback"); } }, t("Details")), - (0, import_react36.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react36.createElement)(CloseIcon)) + (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react37.createElement)(CloseIcon)) ); } var FEEDBACK_CSS = ` @@ -15876,7 +16074,7 @@ var FEEDBACK_CSS = ` `; // src/client/about.js -var import_react37 = require("react"); +var import_react38 = require("react"); var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); function botHint(update) { if (update.phase === "installing") return t("Updating FlyBot core…"); @@ -15911,9 +16109,9 @@ function dshHint(dsh, shown) { var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { const update = useUpdate((value) => value) ?? {}; - const [checking, setChecking] = (0, import_react37.useState)(false); - const [stars, setStars] = (0, import_react37.useState)(null); - (0, import_react37.useEffect)(() => { + const [checking, setChecking] = (0, import_react38.useState)(false); + const [stars, setStars] = (0, import_react38.useState)(null); + (0, import_react38.useEffect)(() => { let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); @@ -15934,29 +16132,29 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { const dshPhase = dsh?.phase ?? "idle"; const dshNew = showsDshRow(dsh, update.app); const dshShown = dshNew && dsh.available; - const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react37.createElement)( + const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react38.createElement)( "button", { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") - ) : (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); - const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react37.createElement)( + ) : (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); + const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react38.createElement)( "button", { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") ) : null; const repo = update.repoUrl; return [ - (0, import_react37.createElement)( + (0, import_react38.createElement)( "div", { key: "hero", className: "bt-about-hero" }, - (0, import_react37.createElement)("span", { className: "bt-about-mark" }, (0, import_react37.createElement)(FlyBotMark, { size: 52 })), - (0, import_react37.createElement)( + (0, import_react38.createElement)("span", { className: "bt-about-mark" }, (0, import_react38.createElement)(FlyBotMark, { size: 52 })), + (0, import_react38.createElement)( "div", { className: "bt-about-copy" }, - (0, import_react37.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), - update.current ? (0, import_react37.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null + (0, import_react38.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), + update.current ? (0, import_react38.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null ), - repo ? (0, import_react37.createElement)( + repo ? (0, import_react38.createElement)( "a", { className: "bt-star", @@ -15966,23 +16164,23 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { title: t("Star FlyBot on GitHub"), "aria-label": stars === null ? t("Star FlyBot on GitHub") : `${t("Star FlyBot on GitHub")} · ${t("{count} stars", { count: stars })}` }, - (0, import_react37.createElement)("span", { className: "bt-star-main" }, (0, import_react37.createElement)("span", { className: "bt-star-icon" }, (0, import_react37.createElement)(StarLineIcon)), "Star"), - stars === null ? null : (0, import_react37.createElement)("span", { className: "bt-star-count" }, starCount(stars)) + (0, import_react38.createElement)("span", { className: "bt-star-main" }, (0, import_react38.createElement)("span", { className: "bt-star-icon" }, (0, import_react38.createElement)(StarLineIcon)), "Star"), + stars === null ? null : (0, import_react38.createElement)("span", { className: "bt-star-count" }, starCount(stars)) ) : null ), - repo ? (0, import_react37.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, - (0, import_react37.createElement)( + repo ? (0, import_react38.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, + (0, import_react38.createElement)( Section, { key: "updates", title: t("Updates") }, - (0, import_react37.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react37.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), - dsh?.current ? (0, import_react37.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null + (0, import_react38.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react38.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), + dsh?.current ? (0, import_react38.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null ), - (0, import_react37.createElement)( + (0, import_react38.createElement)( Section, { key: "links", title: t("Links") }, - repo ? (0, import_react37.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react37.createElement)("a", linkProps(repo), (0, import_react37.createElement)(GitHubMark), t("Open"))) : null, - update.notesUrl ? (0, import_react37.createElement)(Row3, { label: t("Release notes") }, (0, import_react37.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, - (0, import_react37.createElement)(Row3, { label: t("Report a problem") }, (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) + repo ? (0, import_react38.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react38.createElement)("a", linkProps(repo), (0, import_react38.createElement)(GitHubMark), t("Open"))) : null, + update.notesUrl ? (0, import_react38.createElement)(Row3, { label: t("Release notes") }, (0, import_react38.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, + (0, import_react38.createElement)(Row3, { label: t("Report a problem") }, (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) ) ]; } @@ -16011,19 +16209,19 @@ var ABOUT_CSS = ` `; // src/client/settings.js -var import_react38 = require("react"); +var import_react39 = require("react"); var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; function SetSection({ title, children }) { - return (0, import_react38.createElement)("section", { className: "bt-set-section" }, (0, import_react38.createElement)("h3", null, title), (0, import_react38.createElement)("div", { className: "bt-set-card" }, children)); + return (0, import_react39.createElement)("section", { className: "bt-set-section" }, (0, import_react39.createElement)("h3", null, title), (0, import_react39.createElement)("div", { className: "bt-set-card" }, children)); } function SetRow({ label, hint, children }) { - return (0, import_react38.createElement)( + return (0, import_react39.createElement)( "div", { className: "bt-set-row" }, - (0, import_react38.createElement)("div", { className: "bt-set-copy" }, (0, import_react38.createElement)("span", null, label), hint ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, hint) : null), - children ? (0, import_react38.createElement)("div", { className: "bt-set-control" }, children) : null + (0, import_react39.createElement)("div", { className: "bt-set-copy" }, (0, import_react39.createElement)("span", null, label), hint ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, hint) : null), + children ? (0, import_react39.createElement)("div", { className: "bt-set-control" }, children) : null ); } var NOTIFY_KINDS = [ @@ -16035,35 +16233,35 @@ function GeneralPage({ roster, actions, run }) { const prefs = roster.prefs ?? {}; const themes = allThemes(); const current = themes[prefs.theme] ? prefs.theme : "deepseek"; - const [custom, setCustom] = (0, import_react38.useState)(prefs.accent ?? "#4d6bfe"); - const timer = (0, import_react38.useRef)(0); - (0, import_react38.useEffect)(() => () => clearTimeout(timer.current), []); + const [custom, setCustom] = (0, import_react39.useState)(prefs.accent ?? "#4d6bfe"); + const timer = (0, import_react39.useRef)(0); + (0, import_react39.useEffect)(() => () => clearTimeout(timer.current), []); const pickCustom = (value) => { setCustom(value); clearTimeout(timer.current); timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350); }; return [ - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetSection, { key: "look", title: t("Appearance") }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetRow, { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "select", { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, - Object.entries(themes).map(([id, theme]) => (0, import_react38.createElement)("option", { key: id, value: id }, t(theme.label))) + Object.entries(themes).map(([id, theme]) => (0, import_react39.createElement)("option", { key: id, value: id }, t(theme.label))) ) ), - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetRow, { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "div", { className: "bt-swatches" }, - (0, import_react38.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), - ACCENT_INKS.map((ink) => (0, import_react38.createElement)("button", { + (0, import_react39.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), + ACCENT_INKS.map((ink) => (0, import_react39.createElement)("button", { key: ink, type: "button", className: "bt-swatch", @@ -16073,26 +16271,26 @@ function GeneralPage({ roster, actions, run }) { style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), - (0, import_react38.createElement)( + (0, import_react39.createElement)( "label", { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, - (0, import_react38.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) + (0, import_react39.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) ) ) ) ), // One switch for every system notification (#353), then one per kind (#469). - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetSection, { key: "notify", title: t("Notifications") }, - (0, import_react38.createElement)(SwitchRow, { + (0, import_react39.createElement)(SwitchRow, { label: t("Notifications"), showLabel: false, hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), on: prefs.notifications !== false, onChange: (on) => run(actions.setPrefs({ notifications: on })) }), - NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react38.createElement)(SwitchRow, { + NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react39.createElement)(SwitchRow, { key: kind, label: t(label), hint: hint ? t(hint) : void 0, @@ -16108,52 +16306,52 @@ function MainBotsSection({ roster, actions, run }) { const [first, ...others] = mains; const own = roster.bots.filter((bot) => !bot.remote); const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); - return (0, import_react38.createElement)( + return (0, import_react39.createElement)( SetSection, { title: t("Main Bot") }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetRow, { label: t("Main Bot"), hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 }, - (0, import_react38.createElement)("select", { + (0, import_react39.createElement)("select", { className: "bt-select", "aria-label": t("Main Bot"), value: first?.id ?? "", onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) - }, first ? null : (0, import_react38.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react38.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) + }, first ? null : (0, import_react39.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react39.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ), - others.map((bot) => (0, import_react38.createElement)( + others.map((bot) => (0, import_react39.createElement)( SetRow, { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, - (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), - (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) )), - candidates.length > 0 ? (0, import_react38.createElement)( + candidates.length > 0 ? (0, import_react39.createElement)( SetRow, { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, - (0, import_react38.createElement)("select", { + (0, import_react39.createElement)("select", { className: "bt-select", "aria-label": t("Add a Main Bot"), value: "", onChange: (event) => { if (event.target.value) void run(actions.setMain(event.target.value, true)); } - }, (0, import_react38.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react38.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) + }, (0, import_react39.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react39.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ) : null ); } var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]]; function MotionSection({ roster, actions, run }) { const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; - return (0, import_react38.createElement)( + return (0, import_react39.createElement)( SetSection, { title: t("Animation") }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetRow, { label: t("Bot animation") }, - (0, import_react38.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react38.createElement)("button", { + (0, import_react39.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react39.createElement)("button", { key: id, type: "button", "aria-pressed": motion === id, @@ -16165,69 +16363,69 @@ function MotionSection({ roster, actions, run }) { ); } function BotsPage({ roster, actions, run }) { - const [creating, setCreating] = (0, import_react38.useState)(false); + const [creating, setCreating] = (0, import_react39.useState)(false); return [ - (0, import_react38.createElement)(MainBotsSection, { key: "main", roster, actions, run }), - (0, import_react38.createElement)(MotionSection, { key: "motion", roster, actions, run }), - creating ? (0, import_react38.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react38.createElement)( + (0, import_react39.createElement)(MainBotsSection, { key: "main", roster, actions, run }), + (0, import_react39.createElement)(MotionSection, { key: "motion", roster, actions, run }), + creating ? (0, import_react39.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react39.createElement)( "div", { key: "new", className: "bt-actions" }, - (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react38.createElement)(PlusIcon), t("New Bot")) + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react39.createElement)(PlusIcon), t("New Bot")) ), - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetSection, { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, - roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react38.createElement)( + roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react39.createElement)( "button", { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "span", { className: "bt-set-bot" }, - (0, import_react38.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), - (0, import_react38.createElement)("span", { className: "bt-row-title" }, bot.name), - bot.role ? (0, import_react38.createElement)("span", { className: "bt-tag" }, bot.role) : null, - isMainOf(roster, bot.id) ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, - bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null + (0, import_react39.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), + (0, import_react39.createElement)("span", { className: "bt-row-title" }, bot.name), + bot.role ? (0, import_react39.createElement)("span", { className: "bt-tag" }, bot.role) : null, + isMainOf(roster, bot.id) ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, + bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ), - (0, import_react38.createElement)("span", { className: "bt-chevron" }, (0, import_react38.createElement)(ChevronRightIcon)) + (0, import_react39.createElement)("span", { className: "bt-chevron" }, (0, import_react39.createElement)(ChevronRightIcon)) )) ) ]; } function GroupsPage({ roster, actions, run }) { - const [creating, setCreating] = (0, import_react38.useState)(false); + const [creating, setCreating] = (0, import_react39.useState)(false); return [ - creating ? (0, import_react38.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react38.createElement)( + creating ? (0, import_react39.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react39.createElement)( "div", { key: "new", className: "bt-actions" }, - (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react38.createElement)(PlusIcon), t("Create group chat")) + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react39.createElement)(PlusIcon), t("Create group chat")) ), - (0, import_react38.createElement)( + (0, import_react39.createElement)( SetSection, { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, - roster.rooms.length === 0 ? (0, import_react38.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { + roster.rooms.length === 0 ? (0, import_react39.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { const admin = roster.byId[room2.admin]; - return (0, import_react38.createElement)( + return (0, import_react39.createElement)( "button", { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "span", { className: "bt-set-bot" }, - (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), - (0, import_react38.createElement)("span", { className: "bt-row-title" }, room2.name), - (0, import_react38.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), - admin ? (0, import_react38.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react38.createElement)(AdminStar), admin.name) : null, - room2.hidden ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null + (0, import_react39.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react39.createElement)("span", { className: "bt-row-title" }, room2.name), + (0, import_react39.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), + admin ? (0, import_react39.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react39.createElement)(AdminStar), admin.name) : null, + room2.hidden ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ), - (0, import_react38.createElement)("span", { className: "bt-chevron" }, (0, import_react38.createElement)(ChevronRightIcon)) + (0, import_react39.createElement)("span", { className: "bt-chevron" }, (0, import_react39.createElement)(ChevronRightIcon)) ); }) ) ]; } function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { - const [error, setError] = (0, import_react38.useState)(""); - const close = (0, import_react38.useCallback)(() => actions.closeSettings(), []); + const [error, setError] = (0, import_react39.useState)(""); + const close = (0, import_react39.useCallback)(() => actions.closeSettings(), []); useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); @@ -16248,34 +16446,34 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false const nav = useGlide(shown, { axis: "y" }); const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; const depth = bot || room2 ? 1 : 0; - const last = (0, import_react38.useRef)({ key: pageKey, depth, move: "up" }); + const last = (0, import_react39.useRef)({ key: pageKey, depth, move: "up" }); if (last.current.key !== pageKey) { last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; } - const [usageOpened, setUsageOpened] = (0, import_react38.useState)(shown === "usage"); + const [usageOpened, setUsageOpened] = (0, import_react39.useState)(shown === "usage"); if (shown === "usage" && !usageOpened) setUsageOpened(true); - const errorLine = error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; - return (0, import_react38.createElement)( + const errorLine = error ? (0, import_react39.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; + return (0, import_react39.createElement)( "div", { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, - (0, import_react38.createElement)("div", { className: "bt-scrim", onMouseDown: close }), - (0, import_react38.createElement)( + (0, import_react39.createElement)("div", { className: "bt-scrim", onMouseDown: close }), + (0, import_react39.createElement)( "div", { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "nav", { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, - (0, import_react38.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), - pages.map(([key, text]) => (0, import_react38.createElement)("button", { + (0, import_react39.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), + pages.map(([key, text]) => (0, import_react39.createElement)("button", { key, type: "button", className: "bt-settings-item", "aria-current": shown === key ? "page" : void 0, onClick: () => actions.openSettings(key) - }, (0, import_react38.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))), + }, (0, import_react39.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))), // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). - (0, import_react38.createElement)("button", { + (0, import_react39.createElement)("button", { type: "button", className: "bt-settings-item bt-settings-shell", title: t("Opens DeepSeek Harness's own settings (models, providers, permissions) in a separate window"), @@ -16284,51 +16482,51 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false close(); openHarnessSettings(); } - }, (0, import_react38.createElement)(SlidersIcon), t("Harness settings"), (0, import_react38.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) + }, (0, import_react39.createElement)(SlidersIcon), t("Harness settings"), (0, import_react39.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) ), - (0, import_react38.createElement)( + (0, import_react39.createElement)( "div", { className: "bt-settings-main" }, - (0, import_react38.createElement)( + (0, import_react39.createElement)( "div", { className: "bt-settings-head" }, - back ? (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react38.createElement)(ChevronLeftIcon)) : null, - (0, import_react38.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) + back ? (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react39.createElement)(ChevronLeftIcon)) : null, + (0, import_react39.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) ), - shown === "usage" ? null : (0, import_react38.createElement)( + shown === "usage" ? null : (0, import_react39.createElement)( "div", { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, errorLine, - shown === "general" ? (0, import_react38.createElement)(GeneralPage, { roster, actions, run }) : null, - bot ? (0, import_react38.createElement)("div", { className: "bt-settings-form" }, (0, import_react38.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, - shown === "bots" && !bot ? (0, import_react38.createElement)(BotsPage, { roster, actions, run }) : null, - room2 ? (0, import_react38.createElement)("div", { className: "bt-settings-form" }, (0, import_react38.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, - shown === "groups" && !room2 ? (0, import_react38.createElement)(GroupsPage, { roster, actions, run }) : null, - shown === "connectors" ? (0, import_react38.createElement)(ConnectorsPage, { roster, actions }) : null, - pluginPage ? (0, import_react38.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, - shown === "capabilities" ? (0, import_react38.createElement)(CapabilitiesPage, { roster, actions, run }) : null, - shown === "schedules" ? (0, import_react38.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, - shown === "secrets" ? (0, import_react38.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, - shown === "feedback" ? (0, import_react38.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, - shown === "about" ? (0, import_react38.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null + shown === "general" ? (0, import_react39.createElement)(GeneralPage, { roster, actions, run }) : null, + bot ? (0, import_react39.createElement)("div", { className: "bt-settings-form" }, (0, import_react39.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, + shown === "bots" && !bot ? (0, import_react39.createElement)(BotsPage, { roster, actions, run }) : null, + room2 ? (0, import_react39.createElement)("div", { className: "bt-settings-form" }, (0, import_react39.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, + shown === "groups" && !room2 ? (0, import_react39.createElement)(GroupsPage, { roster, actions, run }) : null, + shown === "connectors" ? (0, import_react39.createElement)(ConnectorsPage, { roster, actions }) : null, + pluginPage ? (0, import_react39.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, + shown === "capabilities" ? (0, import_react39.createElement)(CapabilitiesPage, { roster, actions, run }) : null, + shown === "schedules" ? (0, import_react39.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, + shown === "secrets" ? (0, import_react39.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, + shown === "feedback" ? (0, import_react39.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, + shown === "about" ? (0, import_react39.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null ), - usageOpened ? (0, import_react38.createElement)( + usageOpened ? (0, import_react39.createElement)( "div", { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, shown === "usage" ? errorLine : null, - (0, import_react38.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) + (0, import_react39.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) ) : null ), - (0, import_react38.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react38.createElement)(CloseIcon)) + (0, import_react39.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react39.createElement)(CloseIcon)) ) ); } // src/client/context-menu.js -var import_react39 = require("react"); +var import_react40 = require("react"); function ContextMenu({ menu, roster, actions, leaving = false }) { useEscape(() => actions.closeMenu()); - (0, import_react39.useEffect)(() => { + (0, import_react40.useEffect)(() => { const onDown = (event) => { if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); }; @@ -16336,14 +16534,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { return () => window.removeEventListener("mousedown", onDown); }, []); const structure = useSidebarStructure(); - const ref = (0, import_react39.useRef)(null); - const [place, setPlace] = (0, import_react39.useState)({ left: menu.x, top: menu.y }); - (0, import_react39.useLayoutEffect)(() => { + const ref = (0, import_react40.useRef)(null); + const [place, setPlace] = (0, import_react40.useState)({ left: menu.x, top: menu.y }); + (0, import_react40.useLayoutEffect)(() => { const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); }, [menu.x, menu.y]); - if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react39.createElement)(AgentMenu, { menu, roster, actions, leaving }); + if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react40.createElement)(AgentMenu, { menu, roster, actions, leaving }); const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; @@ -16385,14 +16583,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { ] ]; const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); - return (0, import_react39.createElement)( + return (0, import_react40.createElement)( "div", { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, - shown.map((group, index) => (0, import_react39.createElement)( - import_react39.Fragment, + shown.map((group, index) => (0, import_react40.createElement)( + import_react40.Fragment, { key: index }, - index > 0 ? (0, import_react39.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, danger]) => (0, import_react39.createElement)("button", { + index > 0 ? (0, import_react40.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, danger]) => (0, import_react40.createElement)("button", { key: label, type: "button", role: "menuitem", @@ -16401,14 +16599,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } - }, (0, import_react39.createElement)(Icon), label)) + }, (0, import_react40.createElement)(Icon), label)) )) ); } function AgentMenu({ menu, roster, actions, leaving }) { - const ref = (0, import_react39.useRef)(null); - const [place, setPlace] = (0, import_react39.useState)({ left: menu.x, top: menu.y }); - (0, import_react39.useLayoutEffect)(() => { + const ref = (0, import_react40.useRef)(null); + const [place, setPlace] = (0, import_react40.useState)({ left: menu.x, top: menu.y }); + (0, import_react40.useLayoutEffect)(() => { const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); @@ -16430,14 +16628,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; - return (0, import_react39.createElement)( + return (0, import_react40.createElement)( "div", { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, - groups.map((group, index) => (0, import_react39.createElement)( - import_react39.Fragment, + groups.map((group, index) => (0, import_react40.createElement)( + import_react40.Fragment, { key: index }, - index > 0 ? (0, import_react39.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, danger]) => (0, import_react39.createElement)("button", { + index > 0 ? (0, import_react40.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, + group.map(([label, Icon, run, danger]) => (0, import_react40.createElement)("button", { key: label, type: "button", role: "menuitem", @@ -16446,7 +16644,7 @@ function AgentMenu({ menu, roster, actions, leaving }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } - }, (0, import_react39.createElement)(Icon), label)) + }, (0, import_react40.createElement)(Icon), label)) )) ); } @@ -16532,7 +16730,7 @@ function migrateStorage(storage = globalThis.localStorage) { } // src/client/details-drawer.js -var import_react40 = require("react"); +var import_react41 = require("react"); function transcriptOf(list, id, name, hiddenTurns = []) { const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; if (!Array.isArray(outline)) return ""; @@ -16557,10 +16755,10 @@ var EMPTY_TABS = { ids: [], active: null }; var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { - const [pending, setPending] = (0, import_react40.useState)(null); + const [pending, setPending] = (0, import_react41.useState)(null); const catalog = roster.permissions; const saved = bot.permission ?? catalog?.defaultPreset; - (0, import_react40.useEffect)(() => { + (0, import_react41.useEffect)(() => { if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; @@ -16570,14 +16768,14 @@ function PermissionRow({ bot, roster, actions, run }) { setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { className: "bt-drawer-permission" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Permissions")), - (0, import_react40.createElement)( + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Permissions")), + (0, import_react41.createElement)( Segmented, { label: t("Permissions"), value: current }, - catalog.options.map((option) => (0, import_react40.createElement)("button", { + catalog.options.map((option) => (0, import_react41.createElement)("button", { key: option.value, type: "button", "aria-pressed": option.value === current, @@ -16591,42 +16789,42 @@ function PermissionRow({ bot, roster, actions, run }) { function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); - (0, import_react40.useEffect)(() => { + (0, import_react41.useEffect)(() => { actions.browserEnsure?.(bot.id); }, [bot.id]); if (roster.browser !== true || !actions.browserShow) { - return (0, import_react40.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); + return (0, import_react41.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); } if (state.ids.length === 0) { - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { className: "bt-browser-empty-mark" }, - (0, import_react40.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), - (0, import_react40.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), - (0, import_react40.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), - (0, import_react40.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) + (0, import_react41.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), + (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), + (0, import_react41.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), + (0, import_react41.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) ); } - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { className: "bt-bcards" }, state.ids.map((tabId, index) => { const page = pages[tabId] ?? {}; const label = page.title || shortUrl(page.url) || t("New tab"); - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "button", { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, - page.excerpt ? (0, import_react40.createElement)( + page.excerpt ? (0, import_react41.createElement)( "span", { className: "bt-bcard-preview" }, - (0, import_react40.createElement)("span", { className: "bt-bcard-ptitle" }, label), - (0, import_react40.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) - ) : (0, import_react40.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react40.createElement)(GlobeIcon), (0, import_react40.createElement)("span", null, label)) + (0, import_react41.createElement)("span", { className: "bt-bcard-ptitle" }, label), + (0, import_react41.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) + ) : (0, import_react41.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react41.createElement)(GlobeIcon), (0, import_react41.createElement)("span", null, label)) ), - (0, import_react40.createElement)("button", { + (0, import_react41.createElement)("button", { type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), @@ -16635,36 +16833,36 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow void actions.browserCloseTab(bot.id, tabId); } }, "×"), - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { className: "bt-bcard-row" }, - (0, import_react40.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), - (0, import_react40.createElement)("span", { className: "bt-bcard-title" }, label), - page.url ? (0, import_react40.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null + (0, import_react41.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), + (0, import_react41.createElement)("span", { className: "bt-bcard-title" }, label), + page.url ? (0, import_react41.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null ) ); }), - (0, import_react40.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react40.createElement)(PlusIcon), t("New tab")) + (0, import_react41.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react41.createElement)(PlusIcon), t("New tab")) ); } function About({ text, title = "About" }) { - const [open, setOpen] = (0, import_react40.useState)(false); + const [open, setOpen] = (0, import_react41.useState)(false); const long = text.split("\n").length > 5 || text.length > 240; - const body = (0, import_react40.useRef)(null); + const body = (0, import_react41.useRef)(null); useMorph(body, open); - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", null, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t(title)), - (0, import_react40.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), - long ? (0, import_react40.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t(title)), + (0, import_react41.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), + long ? (0, import_react41.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ); } function SharedRoomApprovals({ room: room2 }) { - const [items, setItems] = (0, import_react40.useState)(room2.pendingApprovals ?? []); - const [busy, setBusy] = (0, import_react40.useState)(""); + const [items, setItems] = (0, import_react41.useState)(room2.pendingApprovals ?? []); + const [busy, setBusy] = (0, import_react41.useState)(""); const face = room2.origin === "face-to-face"; - (0, import_react40.useEffect)(() => { + (0, import_react41.useEffect)(() => { const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; if (haveItems) setItems(room2.pendingApprovals); if (room2.viewerRole !== "invited-owner" && !face) return void 0; @@ -16690,28 +16888,28 @@ function SharedRoomApprovals({ room: room2 }) { setBusy(""); } }; - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react40.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), - items.map((item) => (0, import_react40.createElement)( + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react41.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), + items.map((item) => (0, import_react41.createElement)( "div", { key: item.id, className: "bt-set-row" }, - (0, import_react40.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), - (0, import_react40.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), - (0, import_react40.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) + (0, import_react41.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), + (0, import_react41.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), + (0, import_react41.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) )) ); } function FaceMembers({ room: room2, roster, actions }) { const people2 = facePeople(room2, roster); const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", null, - (0, import_react40.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), - (0, import_react40.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react40.createElement)(FacePersonRow, { + (0, import_react41.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), + (0, import_react41.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react41.createElement)(FacePersonRow, { key: person.key, person, roster, @@ -16735,79 +16933,79 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti }; const face = room2.origin === "face-to-face"; return [ - (0, import_react40.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), - invited && room2.sharedFrom && !face ? (0, import_react40.createElement)( + (0, import_react41.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), + invited && room2.sharedFrom && !face ? (0, import_react41.createElement)( "div", { key: "from", className: "bt-shared-from" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, title), - (0, import_react40.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), - room2.sharedFrom.invitedAt ? (0, import_react40.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, - (0, import_react40.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) - ) : face ? null : room2.notice ? (0, import_react40.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react40.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), - face ? null : (0, import_react40.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), - face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react40.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react40.createElement)( + (0, import_react41.createElement)("div", { className: "bt-section-title" }, title), + (0, import_react41.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), + room2.sharedFrom.invitedAt ? (0, import_react41.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, + (0, import_react41.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) + ) : face ? null : room2.notice ? (0, import_react41.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react41.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), + face ? null : (0, import_react41.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), + face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react41.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react41.createElement)( "div", { key: "members" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), - (0, import_react40.createElement)("div", { className: "bt-members", role: "list" }, [ - ...humans.map((person) => (0, import_react40.createElement)( + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), + (0, import_react41.createElement)("div", { className: "bt-members", role: "list" }, [ + ...humans.map((person) => (0, import_react41.createElement)( "div", { key: person.id, role: "listitem" }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { className: "bt-member" }, - (0, import_react40.createElement)("span", { className: "bt-member-name" }, (0, import_react40.createElement)("span", null, person.displayName || person.id)), - (0, import_react40.createElement)("span", { className: "bt-chip" }, t("Human")), - person.role === "admin" ? (0, import_react40.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null + (0, import_react41.createElement)("span", { className: "bt-member-name" }, (0, import_react41.createElement)("span", null, person.displayName || person.id)), + (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Human")), + person.role === "admin" ? (0, import_react41.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ) )), - ...remotes.map((person) => (0, import_react40.createElement)( + ...remotes.map((person) => (0, import_react41.createElement)( "div", { key: person.handle || person.id, role: "listitem" }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { className: "bt-member" }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "span", { className: "bt-member-name" }, - (0, import_react40.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), - (0, import_react40.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) + (0, import_react41.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), + (0, import_react41.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) ), - face ? null : (0, import_react40.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) + face ? null : (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) ) )), ...members.map((member) => { const entry = entries2.find((item) => item.id === member.id); - return (0, import_react40.createElement)( + return (0, import_react41.createElement)( "div", { key: member.id, role: "listitem" }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { className: "bt-member" }, - (0, import_react40.createElement)( + (0, import_react41.createElement)( "button", { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, - (0, import_react40.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), - (0, import_react40.createElement)("span", null, member.name) + (0, import_react41.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), + (0, import_react41.createElement)("span", null, member.name) ), - room2.admin === member.id ? (0, import_react40.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null + room2.admin === member.id ? (0, import_react41.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ), - entry ? (0, import_react40.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null + entry ? (0, import_react41.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null ); }) ]) ), - face ? null : (0, import_react40.createElement)( + face ? null : (0, import_react41.createElement)( "div", { key: "mode" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Who answers")), - (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Who answers")), + (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) ), - face || !usage ? null : (0, import_react40.createElement)( + face || !usage ? null : (0, import_react41.createElement)( "div", { key: "usage" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Usage")), - (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, room2.name), (0, import_react40.createElement)("span", null, usage)) + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Usage")), + (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, room2.name), (0, import_react41.createElement)("span", null, usage)) ) ]; } @@ -16815,15 +17013,15 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const owner = roster.byId[sessionId]; const part = owner ? currentPart(owner) : sessionId; const isMainBot = isMainOf(roster, owner?.id); - const [tab, setTab] = (0, import_react40.useState)("details"); + const [tab, setTab] = (0, import_react41.useState)("details"); const room2 = roster.roomsById[sessionId]; - const [view, setView] = (0, import_react40.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); - const [editing, setEditing] = (0, import_react40.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); - const [draftName, setDraftName] = (0, import_react40.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); - const [copied, setCopied] = (0, import_react40.useState)(false); - const [error, setError] = (0, import_react40.useState)(""); + const [view, setView] = (0, import_react41.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); + const [editing, setEditing] = (0, import_react41.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); + const [draftName, setDraftName] = (0, import_react41.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); + const [copied, setCopied] = (0, import_react41.useState)(false); + const [error, setError] = (0, import_react41.useState)(""); const glide = useGlide(tab); - const last = (0, import_react40.useRef)({ view, tab, move: void 0 }); + const last = (0, import_react41.useRef)({ view, tab, move: void 0 }); if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; @@ -16848,43 +17046,43 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setTimeout(() => setCopied(false), 1600); }); }; - const errorLine = error ? (0, import_react40.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; + const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; if ((bot || room2) && view === "settings" && !bot?.remote) { return [ - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { key: "head", className: "bt-subhead", "data-move": move }, - (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react40.createElement)(ChevronLeftIcon)), - (0, import_react40.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), - hasStrip ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react40.createElement)(CloseIcon)) + (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react41.createElement)(ChevronLeftIcon)), + (0, import_react41.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), + hasStrip ? null : (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) ), - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, errorLine, - bot ? (0, import_react40.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react40.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) + bot ? (0, import_react41.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react41.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) ) ]; } - const top = (0, import_react40.createElement)( + const top = (0, import_react41.createElement)( "div", { className: "bt-drawer-top", "data-move": side }, - bot || room2 ? (0, import_react40.createElement)("button", { + bot || room2 ? (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") - }, (0, import_react40.createElement)(GearIcon)) : null, - bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react40.createElement)(GlobeIcon)) : null, - bot || room2 ? (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react40.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react40.createElement)(ShareIcon)) : null, - hasStrip ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react40.createElement)(CloseIcon)) + }, (0, import_react41.createElement)(GearIcon)) : null, + bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react41.createElement)(GlobeIcon)) : null, + bot || room2 ? (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react41.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react41.createElement)(ShareIcon)) : null, + hasStrip ? null : (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) ); if (!bot && !room2) { return [ - (0, import_react40.createElement)(import_react40.Fragment, { key: "top" }, top), - (0, import_react40.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react40.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) + (0, import_react41.createElement)(import_react41.Fragment, { key: "top" }, top), + (0, import_react41.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react41.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) ]; } const current = bot?.name ?? room2.name; @@ -16898,12 +17096,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ - (0, import_react40.createElement)(import_react40.Fragment, { key: "top" }, top), - (0, import_react40.createElement)( + (0, import_react41.createElement)(import_react41.Fragment, { key: "top" }, top), + (0, import_react41.createElement)( "div", { key: "id", className: "bt-drawer-id", "data-move": side }, - bot ? (0, import_react40.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react40.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - bot?.remote ? (0, import_react40.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react40.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react40.createElement)("input", { + bot ? (0, import_react41.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), + bot?.remote ? (0, import_react41.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react41.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react41.createElement)("input", { className: "bt-drawer-name-input", autoFocus: true, value: draftName, @@ -16918,59 +17116,59 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setEditing(false); } } - }) : (0, import_react40.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { + }) : (0, import_react41.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { setDraftName(room2 && !room2.named ? "" : current); setEditing(true); } }, current), - subtitle ? (0, import_react40.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react40.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null + subtitle ? (0, import_react41.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react41.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null ), - bot?.remote ? null : (0, import_react40.createElement)( + bot?.remote ? null : (0, import_react41.createElement)( "div", { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, - (0, import_react40.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - tabs.map((key) => (0, import_react40.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) + (0, import_react41.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + tabs.map((key) => (0, import_react41.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) ), - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, errorLine, - room2 && tab === "details" ? (0, import_react40.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, + room2 && tab === "details" ? (0, import_react41.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, bot && bot.remote && tab === "details" ? [ - bot.remote.handle ? (0, import_react40.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, - (0, import_react40.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) + bot.remote.handle ? (0, import_react41.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, + (0, import_react41.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) ] : null, bot && !bot.remote && tab === "details" ? [ - (0, import_react40.createElement)( + (0, import_react41.createElement)( "div", { key: "model", className: "bt-drawer-model" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Model")), - (0, import_react40.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), - modelOf(bot, roster) ? null : (0, import_react40.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Model")), + (0, import_react41.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), + modelOf(bot, roster) ? null : (0, import_react41.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) ), - (0, import_react40.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), - (0, import_react40.createElement)( + (0, import_react41.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), + (0, import_react41.createElement)( "div", { key: "folder", className: "bt-drawer-model" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Working folder")), - (0, import_react40.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Working folder")), + (0, import_react41.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) ), - bot.instructions ? (0, import_react40.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react40.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), - roster.memory === false ? null : (0, import_react40.createElement)(MemoryRow, { key: "memory", bot, actions }), - usage ? (0, import_react40.createElement)( + bot.instructions ? (0, import_react41.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react41.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), + roster.memory === false ? null : (0, import_react41.createElement)(MemoryRow, { key: "memory", bot, actions }), + usage ? (0, import_react41.createElement)( "div", { key: "usage" }, - (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Usage")), - (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, bot.name), (0, import_react40.createElement)("span", null, usage)) + (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Usage")), + (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, bot.name), (0, import_react41.createElement)("span", null, usage)) ) : null, - (0, import_react40.createElement)(ScheduleSummary, { key: "routines", bot, actions }) + (0, import_react41.createElement)(ScheduleSummary, { key: "routines", bot, actions }) ] : null, - bot && !bot.remote && tab === "browser" ? (0, import_react40.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null + bot && !bot.remote && tab === "browser" ? (0, import_react41.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null ) ]; } // src/client/browser-pane.js -var import_react41 = require("react"); +var import_react42 = require("react"); var import_react_dom8 = require("react-dom"); var PANEL_LINGER_MS = 300; var WIDTH_KEY = STORAGE.browserWidth; @@ -16980,10 +17178,10 @@ var EMPTY_TABS2 = { ids: [], active: null }; var SPRING_LEAD2 = springEasing(576, 39.4); var SPRING_TRAIL2 = springEasing(144, 19.7); var icon2 = (paths) => function BrowserIcon() { - return (0, import_react41.createElement)( + return (0, import_react42.createElement)( "svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, - paths.map((d, index) => (0, import_react41.createElement)("path", { key: index, d })) + paths.map((d, index) => (0, import_react42.createElement)("path", { key: index, d })) ); }; var GlobeIcon = icon2(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); @@ -16995,9 +17193,9 @@ var CloseIcon2 = icon2(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); var PlusTabIcon = icon2(["M10 5v10M5 10h10"]); var LockIcon2 = icon2(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); function Favicon({ src, className }) { - const [failed, setFailed] = (0, import_react41.useState)(null); - if (!src || failed === src) return (0, import_react41.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react41.createElement)(GlobeIcon)); - return (0, import_react41.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); + const [failed, setFailed] = (0, import_react42.useState)(null); + if (!src || failed === src) return (0, import_react42.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react42.createElement)(GlobeIcon)); + return (0, import_react42.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); } var savedWidth = () => { try { @@ -17056,11 +17254,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); - const wrap = (0, import_react41.useRef)(null); - const ind = (0, import_react41.useRef)(null); - const spot = (0, import_react41.useRef)(null); - const under = (0, import_react41.useRef)(null); - (0, import_react41.useLayoutEffect)(() => { + const wrap = (0, import_react42.useRef)(null); + const ind = (0, import_react42.useRef)(null); + const spot = (0, import_react42.useRef)(null); + const under = (0, import_react42.useRef)(null); + (0, import_react42.useLayoutEffect)(() => { const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; @@ -17089,7 +17287,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, trail.cancel(); }; }); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { @@ -17103,13 +17301,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, observer.observe(box); return () => observer.disconnect(); }, []); - return (0, import_react41.createElement)( + return (0, import_react42.createElement)( "div", { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, - (0, import_react41.createElement)( + (0, import_react42.createElement)( "div", { className: "bt-ptabs-in", ref: wrap }, - (0, import_react41.createElement)( + (0, import_react42.createElement)( "button", { type: "button", @@ -17119,13 +17317,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, title: bot.name, onClick: () => onView("details") }, - (0, import_react41.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), - view === "details" ? (0, import_react41.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null + (0, import_react42.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), + view === "details" ? (0, import_react42.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null ), ids.map((tabId) => { const page = pages[tabId] ?? EMPTY_PAGE; const on = view === "browser" && tabId === active; - return (0, import_react41.createElement)( + return (0, import_react42.createElement)( "button", { key: tabId, @@ -17146,9 +17344,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, } } }, - (0, import_react41.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), - (0, import_react41.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), - (0, import_react41.createElement)("span", { + (0, import_react42.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), + (0, import_react42.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), + (0, import_react42.createElement)("span", { className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), @@ -17160,20 +17358,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, }, "×") ); }), - (0, import_react41.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) + (0, import_react42.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) ), // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. - (0, import_react41.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react41.createElement)(PlusTabIcon)), - (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react41.createElement)(CloseIcon2)) + (0, import_react42.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react42.createElement)(PlusTabIcon)), + (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react42.createElement)(CloseIcon2)) ); } function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE; - const frame = (0, import_react41.useRef)(null); - const input = (0, import_react41.useRef)(null); - const [draft, setDraft] = (0, import_react41.useState)(null); + const frame = (0, import_react42.useRef)(null); + const input = (0, import_react42.useRef)(null); + const [draft, setDraft] = (0, import_react42.useState)(null); const showGuest = landed && !leaving && Boolean(page.url) && !page.error; - (0, import_react41.useLayoutEffect)(() => { + (0, import_react42.useLayoutEffect)(() => { const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); @@ -17199,7 +17397,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { setDraft(null); if (landed && !page.url) input.current?.focus(); }, [tabId, landed]); @@ -17215,27 +17413,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) }; const editing = draft !== null; const secure = /^https:/i.test(page.url); - return (0, import_react41.createElement)( + return (0, import_react42.createElement)( "div", { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, - (0, import_react41.createElement)( + (0, import_react42.createElement)( "div", { className: "bt-browser-bar" }, - (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react41.createElement)(BackIcon)), - (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react41.createElement)(ForwardIcon)), - (0, import_react41.createElement)("button", { + (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react42.createElement)(BackIcon)), + (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react42.createElement)(ForwardIcon)), + (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", disabled: !page.url, "aria-label": page.loading ? t("Stop") : t("Reload"), title: page.loading ? t("Stop") : t("Reload"), onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") - }, (0, import_react41.createElement)(page.loading ? StopIcon : ReloadIcon)), - (0, import_react41.createElement)( + }, (0, import_react42.createElement)(page.loading ? StopIcon : ReloadIcon)), + (0, import_react42.createElement)( "form", { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, - (0, import_react41.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react41.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), - (0, import_react41.createElement)("input", { + (0, import_react42.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react42.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), + (0, import_react42.createElement)("input", { ref: input, type: "text", spellCheck: false, @@ -17259,25 +17457,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) } }) ), - page.loading ? (0, import_react41.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null + page.loading ? (0, import_react42.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null ), - (0, import_react41.createElement)( + (0, import_react42.createElement)( "div", { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, - page.error ? (0, import_react41.createElement)( + page.error ? (0, import_react42.createElement)( "div", { className: "bt-browser-empty" }, - (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), - (0, import_react41.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), - page.url ? (0, import_react41.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { + (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), + (0, import_react42.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), + page.url ? (0, import_react42.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { void actions.browserNavigate(tabId, page.url); } }, t("Try again")) : null - ) : !page.url ? (0, import_react41.createElement)( + ) : !page.url ? (0, import_react42.createElement)( "div", { className: "bt-browser-empty" }, - (0, import_react41.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), - (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), - (0, import_react41.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) + (0, import_react42.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), + (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), + (0, import_react42.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) ) : null ) ); @@ -17289,41 +17487,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); - const root = (0, import_react41.useRef)(null); - const fly = (0, import_react41.useRef)(null); - const [landed, setLanded] = (0, import_react41.useState)(false); - const [settling, setSettling] = (0, import_react41.useState)(false); - const [width, setWidth] = (0, import_react41.useState)(savedWidth); - const [viewport, setViewport] = (0, import_react41.useState)(() => window.innerWidth); - const [dragging, setDragging] = (0, import_react41.useState)(false); - const [resizing, setResizing] = (0, import_react41.useState)(false); + const root = (0, import_react42.useRef)(null); + const fly = (0, import_react42.useRef)(null); + const [landed, setLanded] = (0, import_react42.useState)(false); + const [settling, setSettling] = (0, import_react42.useState)(false); + const [width, setWidth] = (0, import_react42.useState)(savedWidth); + const [viewport, setViewport] = (0, import_react42.useState)(() => window.innerWidth); + const [dragging, setDragging] = (0, import_react42.useState)(false); + const [resizing, setResizing] = (0, import_react42.useState)(false); const instant = dragging || resizing; - const [reading, setReading] = (0, import_react41.useState)(false); + const [reading, setReading] = (0, import_react42.useState)(false); const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); - (0, import_react41.useLayoutEffect)(() => { + (0, import_react42.useLayoutEffect)(() => { if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); }, [view, width]); - (0, import_react41.useLayoutEffect)(() => { + (0, import_react42.useLayoutEffect)(() => { chatRoom.hold(instant); }, [instant]); - const [last, setLast] = (0, import_react41.useState)({ view, width: panelWidth }); + const [last, setLast] = (0, import_react42.useState)({ view, width: panelWidth }); if (last.view !== view || last.width !== panelWidth) { setLast({ view, width: panelWidth }); if (last.view !== view) setSettling(last.width !== panelWidth && !instant); } - const shownView = (0, import_react41.useRef)(view); - (0, import_react41.useLayoutEffect)(() => { + const shownView = (0, import_react42.useRef)(view); + (0, import_react42.useLayoutEffect)(() => { const switched = settling && shownView.current !== view; shownView.current = view; if (switched) chatRoom.hold(true); chatRoom.set(leaving ? null : panelWidth); if (switched) chatRoom.hold(false); }, [view, panelWidth, leaving]); - (0, import_react41.useEffect)(() => () => { + (0, import_react42.useEffect)(() => () => { chatRoom.set(null); chatRoom.hold(false); }, []); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { let timer; const onResize = () => { setResizing(true); @@ -17337,28 +17535,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu window.removeEventListener("resize", onResize); }; }, []); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { if (!settling) return void 0; const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); return () => clearTimeout(timer); }, [settling, view]); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { const timer = setTimeout(() => setLanded(true), 360); return () => clearTimeout(timer); }, []); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { if (canBrowse) actions.browserEnsure(bot.id); }, [bot?.id, canBrowse]); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); }, [view, active, tabState.ids.length]); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { if (view !== "browser" || leaving) void actions.browserShow?.(null); }, [view, leaving]); - (0, import_react41.useEffect)(() => () => { + (0, import_react42.useEffect)(() => () => { void actions.browserShow?.(null); }, []); - (0, import_react41.useEffect)(() => { + (0, import_react42.useEffect)(() => { if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); @@ -17395,7 +17593,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu return actions[name](...args); }; } - (0, import_react41.useLayoutEffect)(() => { + (0, import_react42.useLayoutEffect)(() => { const move = fly.current; if (!move) return; fly.current = null; @@ -17478,7 +17676,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; - return (0, import_react41.createElement)( + return (0, import_react42.createElement)( "aside", { ref: root, @@ -17495,15 +17693,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu if (event.target === event.currentTarget) setLanded(true); } }, - leaving ? null : (0, import_react41.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), + leaving ? null : (0, import_react42.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), // Over the whole window while dragging: the cursor stays a resize cursor and the // pages underneath (the chat, the guest) see no pointer. - dragging ? (0, import_react_dom8.createPortal)((0, import_react41.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, - hasStrip ? (0, import_react41.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, - view === "browser" && bot ? (0, import_react41.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react41.createElement)( + dragging ? (0, import_react_dom8.createPortal)((0, import_react42.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, + hasStrip ? (0, import_react42.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, + view === "browser" && bot ? (0, import_react42.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react42.createElement)( "div", { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, - (0, import_react41.createElement)(DetailsDrawer, { + (0, import_react42.createElement)(DetailsDrawer, { sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, @@ -17600,7 +17798,7 @@ var BROWSER_CSS = ` `; // src/client/remote-bot-settings.js -var import_react42 = require("react"); +var import_react43 = require("react"); var REMOTE_BOT_SETTINGS_CSS = ` .bt-remote-sheet-layer{position:fixed;inset:0;z-index:60;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} .bt-remote-sheet-layer[data-leaving]{pointer-events:none} @@ -17624,8 +17822,8 @@ var REMOTE_BOT_SETTINGS_CSS = ` function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { const close = () => actions.closeSettings(); useEscape(close); - const dialog = (0, import_react42.useRef)(null); - (0, import_react42.useEffect)(() => { + const dialog = (0, import_react43.useRef)(null); + (0, import_react43.useEffect)(() => { const before = document.activeElement; dialog.current?.focus?.({ preventScroll: true }); return () => { @@ -17640,37 +17838,37 @@ function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { }; if (!bot?.remote) return null; const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); - return (0, import_react42.createElement)( + return (0, import_react43.createElement)( "div", { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, - (0, import_react42.createElement)("div", { className: "bt-scrim", onMouseDown: close }), - (0, import_react42.createElement)( + (0, import_react43.createElement)("div", { className: "bt-scrim", onMouseDown: close }), + (0, import_react43.createElement)( "div", { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, - (0, import_react42.createElement)( + (0, import_react43.createElement)( "div", { className: "bt-remote-sheet-head" }, - (0, import_react42.createElement)(BotMark, { bot, size: 40, live: true }), - (0, import_react42.createElement)( + (0, import_react43.createElement)(BotMark, { bot, size: 40, live: true }), + (0, import_react43.createElement)( "div", { className: "bt-remote-sheet-who" }, - (0, import_react42.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), - (0, import_react42.createElement)("span", { className: "bt-remote-sheet-sub" }, who) + (0, import_react43.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), + (0, import_react43.createElement)("span", { className: "bt-remote-sheet-sub" }, who) ), - (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react42.createElement)(CloseIcon)) + (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) ), - (0, import_react42.createElement)( + (0, import_react43.createElement)( "div", { className: "bt-remote-sheet-body" }, - bot.remote.handle ? (0, import_react42.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, - (0, import_react42.createElement)(FlyBotDetails, { bot, roster, actions }) + bot.remote.handle ? (0, import_react43.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, + (0, import_react43.createElement)(FlyBotDetails, { bot, roster, actions }) ) ) ); } // src/client/overlays.js -var import_react43 = require("react"); +var import_react44 = require("react"); var import_react_dom9 = require("react-dom"); var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; @@ -17679,15 +17877,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); - const previous = (0, import_react43.useRef)(current); - (0, import_react43.useEffect)(() => { + const previous = (0, import_react44.useRef)(current); + (0, import_react44.useEffect)(() => { if (previous.current === current) return; previous.current = current; if (current) actions.followPanel?.(current); }, [current]); const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; - const [kept, setKept] = (0, import_react43.useState)(open); - (0, import_react43.useEffect)(() => { + const [kept, setKept] = (0, import_react44.useState)(open); + (0, import_react44.useEffect)(() => { if (open) { setKept(open); return void 0; @@ -17703,20 +17901,20 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); - return (0, import_react43.createElement)( - import_react43.Fragment, + return (0, import_react44.createElement)( + import_react44.Fragment, null, - pane ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, - ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, - ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, - ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, - ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, - menu ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, - settings ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, - remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, - memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, - inbox ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, - board ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null + pane ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, + ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, + ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, + ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, + ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, + menu ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, + settings ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, + remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, + memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, + inbox ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, + board ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null ); } @@ -18671,7 +18869,7 @@ function writeMode(mode, storage = globalThis.localStorage) { } // src/client/user-message.js -var import_react44 = require("react"); +var import_react45 = require("react"); var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { @@ -18757,38 +18955,38 @@ function userTextParts(text, bots, referenceLabels = []) { return parts; } function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { - const chip = (part) => [(0, import_react44.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; + const chip = (part) => [(0, import_react45.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; return userTextParts(text, bots, referenceLabels).map((part, index) => { - if (!part.bot) return (0, import_react44.createElement)(import_react44.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)); + if (!part.bot) return (0, import_react45.createElement)(import_react45.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)); const title = `@${part.bot.name}`; - return typeof part.bot.id === "string" ? (0, import_react44.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react44.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); + return typeof part.bot.id === "string" ? (0, import_react45.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react45.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); }); } function Attachments({ attachments, renderMessageImages }) { const compact2 = attachments.length > 1; - return (0, import_react44.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { + return (0, import_react45.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { if (block.type === "image") { - return renderMessageImages ? (0, import_react44.createElement)(import_react44.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null; + return renderMessageImages ? (0, import_react45.createElement)(import_react45.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null; } const file = block.attachment; const meta = [ui.fileExtension?.(file.name)?.toUpperCase().slice(0, 8), ui.fileSizeText?.(file.bytes)].filter(Boolean).join(" · "); - return (0, import_react44.createElement)( + return (0, import_react45.createElement)( "span", { key: index, className: "bt-ufile", title: file.name }, - ui.FileTypeIcon ? (0, import_react44.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, - (0, import_react44.createElement)("span", { className: "bt-ufile-text" }, (0, import_react44.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react44.createElement)("span", { className: "bt-ufile-meta" }, meta) : null) + ui.FileTypeIcon ? (0, import_react45.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, + (0, import_react45.createElement)("span", { className: "bt-ufile-text" }, (0, import_react45.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react45.createElement)("span", { className: "bt-ufile-meta" }, meta) : null) ); })); } function CopyButton({ text }) { - const [copied, setCopied] = (0, import_react44.useState)(false); + const [copied, setCopied] = (0, import_react45.useState)(false); const copy2 = () => { void navigator.clipboard?.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1e3); }); }; - return (0, import_react44.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react44.createElement)(CheckIcon) : (0, import_react44.createElement)(CopyIcon)); + return (0, import_react45.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react45.createElement)(CheckIcon) : (0, import_react45.createElement)(CopyIcon)); } var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0; function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) { @@ -18805,7 +19003,7 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma const person = incoming && author.authorKind === "human" && !author.ownerName; const faceMine = Boolean(room2?.origin === "face-to-face" && !incoming); const myName = faceMine ? facePeople(room2, roster).find((entry) => entry.mine)?.name || cleanPeerName(roster.self?.displayName, "") || author?.name || "" : ""; - return (0, import_react44.createElement)( + return (0, import_react45.createElement)( "div", { className: incoming ? "bt-umsg bt-umsg-other" : "bt-umsg", @@ -18815,22 +19013,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma "data-author-kind": author?.authorKind, "data-author-name": author?.name }, - person ? (0, import_react44.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react44.createElement)(PersonMark, { name: author.name })) : null, - faceMine ? (0, import_react44.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react44.createElement)(PersonMark, { name: myName || void 0 })) : null, - (0, import_react44.createElement)( + person ? (0, import_react45.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react45.createElement)(PersonMark, { name: author.name })) : null, + faceMine ? (0, import_react45.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react45.createElement)(PersonMark, { name: myName || void 0 })) : null, + (0, import_react45.createElement)( "div", { className: "bt-ustack" }, - faceMine && myName ? (0, import_react44.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react44.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, - attachments.length > 0 ? (0, import_react44.createElement)(Attachments, { attachments, renderMessageImages }) : null, - text !== "" || rest.length > 0 ? (0, import_react44.createElement)( + faceMine && myName ? (0, import_react45.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react45.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react45.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, + attachments.length > 0 ? (0, import_react45.createElement)(Attachments, { attachments, renderMessageImages }) : null, + text !== "" || rest.length > 0 ? (0, import_react45.createElement)( "div", { className: "bt-ubody", title: clock3(data.time) }, - (0, import_react44.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), - rest.map((block, index) => ui.JsonBlock ? (0, import_react44.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null) + (0, import_react45.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), + rest.map((block, index) => ui.JsonBlock ? (0, import_react45.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null) ) : null, - referenceLabels.length > 0 ? (0, import_react44.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null + referenceLabels.length > 0 ? (0, import_react45.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null ), - text === "" ? null : (0, import_react44.createElement)("div", { className: "bt-utools" }, (0, import_react44.createElement)(CopyButton, { text })) + text === "" ? null : (0, import_react45.createElement)("div", { className: "bt-utools" }, (0, import_react45.createElement)(CopyButton, { text })) ); } var USER_MESSAGE_CSS = ` @@ -18928,7 +19126,7 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since } // src/client/stats-dock.js -var import_react45 = require("react"); +var import_react46 = require("react"); var STATS_DOCK = "conversation.composer.dock"; var STATS_ID = "stats"; var STATS_IDS = [STATS_ID, "activity"]; @@ -18944,17 +19142,17 @@ function pluralStats(t2) { function statsDock(entriesOf, id = STATS_ID) { const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id); function StatsDock(props) { - const t2 = (0, import_react45.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); + const t2 = (0, import_react46.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); const entry = shell(); if (!entry || typeof props.usePerformanceUsage !== "function") return null; - return (0, import_react45.createElement)(entry.component, { ...props, t: t2 }); + return (0, import_react46.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 -var import_react46 = require("react"); +var import_react47 = require("react"); var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; function apply(ctx) { const flybot = createClientApi2(); @@ -19909,7 +20107,7 @@ function apply(ctx) { ]; 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_react46.createElement)(TurnFailures, { ...props, work }) }); + const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react47.createElement)(TurnFailures, { ...props, work }) }); return () => { off(); style.remove(); diff --git a/core/src/client/delivery-chart.js b/core/src/client/delivery-chart.js new file mode 100644 index 00000000..e9ff2131 --- /dev/null +++ b/core/src/client/delivery-chart.js @@ -0,0 +1,243 @@ +// Delivery trend charts (#1212): one 7-day line per countable metric, drawn from `metrics.daily`. +// The host half builds the series (Asia/Shanghai days, the last entry carries `partial: true` until +// the day is over); this module only draws it, and a snapshot without the series says so instead of +// guessing. A day the host could not read breaks the line — an unknown is never drawn as zero. +import { createElement as h } from 'react' +import { t } from './i18n.js' + +export const CHART_WIDTH = 168 +export const CHART_HEIGHT = 28 +const PAD = 3 +const DOT_RADIUS = 2 +// The dot that is not a measurement yet (today, still running) is drawn a touch larger so the +// hollow ring stays visible at this size. +const HOLLOW_RADIUS = 2.5 +// The most days a chart draws; the host sends 7 today and a longer series is cut to its end. +export const CHART_DAYS = 7 + +// Keys line up with the rows `metricRows()` builds, in that order: the first five are countable and +// carry a line, the two medians after them cannot be added up and get no chart at all (#1212). +// The five countable metrics draw a line; the CI share draws one too but is a ratio, so it never +// takes a Σ total. Ties to the rows `metricRows()` builds, by key. +export const CHART_KEYS = ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci'] +export const COUNTING_KEYS = ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests'] + +// The days to draw, or null when the snapshot carries no series at all (host half not landed yet, +// or an older host) — that is the empty state, not seven zeros. +export function chartDays(daily) { + if (!Array.isArray(daily)) return null + const days = daily.filter(day => day && typeof day === 'object') + return days.length ? days.slice(-CHART_DAYS) : null +} + +// True while today is still running: the last day says `partial: true`. +export function todayPartial(daily) { + const days = chartDays(daily) + return Boolean(days && days.length && days[days.length - 1].partial === true) +} + +// One day's number for one metric; null means the host did not get it, which is not a zero. +export function dayValue(day, key) { + if (!day || typeof day !== 'object') return null + if (key === 'ci') { + const failed = day.ci?.failed + const counted = day.ci?.counted + const total = day.ci?.total + // A superseded run is not in the denominator when the host counted them (#1212 contract). + const denominator = Number.isFinite(counted) && counted > 0 + ? counted + : (Number.isFinite(total) && total > 0 ? total : null) + if (!Number.isFinite(failed) || denominator === null) return null + return Math.min(1, Math.max(0, failed / denominator)) + } + const value = day[key] + return Number.isFinite(value) ? value : null +} + +// The calendar date the day stands for, in the zone the host cuts days by (Asia/Shanghai, UTC+8, +// no DST). A start the host did not send leaves the date out of the title, never invents one. +export function dayLabel(day) { + // The contract is a plain date: 'YYYY-MM-DD'; an older answer carried an ISO start, read the + // same way so a host mid-upgrade still labels its days. + if (typeof day?.date === 'string' && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(day.date)) return day.date + const at = Date.parse(day?.start ?? '') + if (!Number.isFinite(at)) return '' + return new Date(at + 8 * 3_600_000).toISOString().slice(0, 10) +} + +// `2026-10-26 · 12`, and `2026-10-26 · 未取到` for a day the host could not read. +export function pointTitle(day, key) { + const date = dayLabel(day) + const value = dayValue(day, key) + const shown = value === null + ? t('not available') + : (key === 'ci' ? `${Math.round(value * 100)}%` : String(value)) + return date ? `${date} · ${shown}` : shown +} + +// How many of the drawn days carry a number for this metric; fewer than the window means the total +// is incomplete and wears the star (#1212). +export function knownDays(daily, key) { + const days = chartDays(daily) ?? [] + return days.filter(day => dayValue(day, key) !== null).length +} + +const round = value => Math.round(value * 100) / 100 + +// The SVG geometry for one metric: one x per day, a y for each known day, and the path split at +// every unknown day. Pure, so the shape can be asserted without React (#1212). +export function chartGeometry(daily, key, { width = CHART_WIDTH, height = CHART_HEIGHT } = {}) { + const days = chartDays(daily) + if (!days) return null + const values = days.map(day => dayValue(day, key)) + const known = values.filter(value => value !== null) + const top = known.length ? Math.max(...known, 0) : 0 + // All-zero days still sit on the floor; a flat line on the baseline is the honest picture, and a + // filled dot there is how a real zero stays apart from a missing day. + const span = top > 0 ? top : 1 + const step = days.length > 1 ? (width - PAD * 2) / (days.length - 1) : 0 + const dots = days.map((day, i) => { + const value = values[i] + const last = i === days.length - 1 + const state = value === null ? 'missing' : (last && day.partial === true ? 'partial' : 'full') + return { + index: i, + x: round(PAD + step * i), + y: value === null ? null : round(PAD + (height - PAD * 2) * (1 - value / span)), + today: last, + // Only the day that is not over yet is drawn hollow; a finished day is a filled dot. + hollow: state === 'partial', + partial: state === 'partial', + state, + title: pointTitle(day, key), + value, + } + }) + const segments = [] + let run = null + for (const dot of dots) { + if (dot.y === null) { + if (run) { segments.push(run); run = null } + continue + } + if (!run) run = { dots: [] } + run.dots.push(dot) + } + if (run) segments.push(run) + return { + width, + height, + dots, + known: known.length, + segments: segments.map(segment => ({ + d: segment.dots.map((dot, i) => `${i ? 'L' : 'M'}${dot.x} ${dot.y}`).join(' '), + // Only the stretch that ends today is the day that is not over yet. + dashed: segment.dots.some(dot => dot.partial), + today: segment.dots.some(dot => dot.today), + })), + } +} + +// One metric's line: 168×28, 1.5px stroke, r=2 dots, a `title` on every point. Each dot says its +// own state (`full` / `partial`); a missing day has no dot at all, so a break can never be read as +// a zero, and a real zero is a filled dot on the baseline (#1212). +export function DeliveryChart({ daily, metricKey, label }) { + const geo = chartGeometry(daily, metricKey) + if (!geo || !geo.known) return null + const name = label ? t('{label} over the last 7 days', { label }) : t('Last 7 days') + return h('svg', { + className: 'bt-del-chart', + 'data-flybot': 'delivery-chart', + 'data-metric': metricKey, + width: geo.width, + height: geo.height, + viewBox: `0 0 ${geo.width} ${geo.height}`, + role: 'img', + 'aria-label': name, + }, + geo.segments.map((segment, i) => h('path', { + key: `s${i}`, + className: 'bt-del-chart-line', + 'data-today': segment.today ? 'true' : undefined, + ...(segment.dashed ? { 'stroke-dasharray': '2 2' } : {}), + d: segment.d, + })), + geo.dots.filter(dot => dot.y !== null).map(dot => h('circle', { + key: `d${dot.index}`, + className: 'bt-del-chart-dot', + 'data-state': dot.state, + 'data-hollow': dot.hollow ? 'true' : undefined, + cx: dot.x, + cy: dot.y, + r: dot.hollow ? HOLLOW_RADIUS : DOT_RADIUS, + }, h('title', null, dot.title)))) +} + +// The metrics block #1212 draws: `label | line | today | 7 days`, one row per metric, the five +// countable lines and the CI share stacked. Rows keep the numbers `metricRows()` already formatted; a total is +// prefixed with Σ and muted so only one number per row is the day's, and it wears a star when the +// window has days the host could not read. The two median rows get no line, no placeholder and no +// total — a median cannot be added up. Below 900px the line moves under its row (DELIVERY_CHART_CSS). +export function DeliveryMetrics({ rows = [], daily }) { + const days = chartDays(daily) + const short = CHART_KEYS + .map(key => knownDays(days, key)) + .filter(count => count < (days?.length ?? 0)) + const fewest = short.length ? Math.min(...short) : 0 + const head = h('div', { className: 'bt-del-dm-head', 'data-flybot': 'delivery-metrics-head' }, + h('span', { className: 'bt-del-dm-label' }, ''), + h('span', { className: 'bt-del-dm-chart-head' }, ''), + h('span', { className: 'bt-del-dm-value' }, t('Today')), + h('span', { className: 'bt-del-dm-value bt-del-dm-week' }, t('7 days'))) + const body = rows.map((row, index) => { + // The row names the series it belongs to, so reordering the rows cannot hand one metric + // another metric's line; a row outside CHART_KEYS (a median) simply gets none. + const key = CHART_KEYS.includes(row?.key) ? row.key : null + const chart = days && key + ? h(DeliveryChart, { key: `c-${key}`, daily: days, metricKey: key, label: row?.label }) + : null + // A ratio is not a sum: the CI share keeps its week number plain, and the star marks an + // incomplete count only. + const counted = key !== null && COUNTING_KEYS.includes(key) + const star = days && counted && knownDays(days, key) < days.length ? '*' : '' + const cells = [ + h('span', { key: 'label', className: 'bt-del-dm-label' }, row?.label ?? ''), + // A median row gets no chart cell at all: no line, no placeholder box, no total. + ...(key ? [h('span', { key: 'chart', className: 'bt-del-dm-chart' }, chart)] : []), + h('span', { key: 'today', className: 'bt-del-dm-value bt-del-dm-today' }, row?.today ?? '—'), + h('span', { key: 'week', className: 'bt-del-dm-value bt-del-dm-week' }, counted ? `Σ ${row?.week ?? '—'}${star}` : (row?.week ?? '—')), + ] + return h('div', { key: row?.label ?? String(index), className: 'bt-del-dm-row', 'data-metric': key ?? undefined }, cells) + }) + return h('div', { className: 'bt-del-dm', 'data-flybot': 'delivery-metrics' }, + head, + body, + days ? null : h('p', { className: 'bt-del-chart-empty', 'data-flybot': 'delivery-chart-empty' }, t('No 7-day data yet.')), + days && short.length + ? h('p', { className: 'bt-del-chart-foot', 'data-flybot': 'delivery-chart-foot' }, t('* {count} of {days} days had data', { count: fewest, days: days.length })) + : null) +} + +export const DELIVERY_CHART_CSS = ` +.bt-del-dm{display:flex;flex-direction:column;gap:2px;min-width:0} +.bt-del-dm-head,.bt-del-dm-row{display:flex;align-items:center;gap:8px} +.bt-del-dm-head{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)} +.bt-del-dm-label{flex:1 1 auto;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-2)} +.bt-del-dm-head .bt-del-dm-label{color:inherit} +.bt-del-dm-chart,.bt-del-dm-chart-head{flex:0 0 168px;display:flex;align-items:center;min-height:28px} +.bt-del-dm-value{flex:0 0 auto;min-width:56px;text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;color:var(--bt-ink)} +.bt-del-dm-head .bt-del-dm-value{color:inherit;font-weight:inherit} +.bt-del-dm-today{color:var(--bt-ink);font-weight:600} +.bt-del-dm-week{color:var(--bt-ink-3);font-weight:400} +.bt-del-chart{display:block;overflow:visible} +.bt-del-chart-line{fill:none;stroke:var(--bt-accent);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round} +.bt-del-chart-dot{fill:var(--bt-accent);stroke:none} +.bt-del-chart-dot[data-state=partial]{fill:none;stroke:var(--bt-accent);stroke-width:1.5} +.bt-del-chart-empty,.bt-del-chart-foot{margin:2px 0 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} +@media (max-width:899.98px){ + .bt-del-dm-head{display:none} + .bt-del-dm-row{flex-wrap:wrap;gap:4px 8px;min-height:44px;align-content:center} + .bt-del-dm-chart{order:5;flex:0 0 100%;min-height:0} + .bt-del-dm-value{min-width:0} +} +` diff --git a/core/src/client/delivery-view.js b/core/src/client/delivery-view.js index 7c08b127..deedba57 100644 --- a/core/src/client/delivery-view.js +++ b/core/src/client/delivery-view.js @@ -8,6 +8,7 @@ // read. The poll is fast while a round is running (2 s) and slow once there is something to read. import { createElement as h, useEffect, useState } from 'react' import { t } from './i18n.js' +import { DeliveryMetrics, DELIVERY_CHART_CSS, todayPartial } from './delivery-chart.js' const POLL_MS = 60_000 // While the host is still building there is nothing to see, so ask often; once the board is up, @@ -129,17 +130,26 @@ export function metricRows(snapshot) { const today = snapshot?.today ?? {} const week = snapshot?.week ?? {} const ci = today.ci ?? null + // The CI share is a ratio of the runs that counted, so it is written as a percentage and is + // never summed; a window with nothing counted reads '—', not 0%. + const share = (entry) => (entry && Number.isFinite(entry.counted) && entry.counted > 0 + ? `${Math.round((entry.failed / entry.counted) * 100)}%` : '—') const count = (value) => (value === null || value === undefined ? '—' : String(value)) const rows = [ { label: t('New issues'), key: 'newIssues' }, { label: t('Closed issues'), key: 'closedIssues' }, { label: t('New pull requests'), key: 'newPullRequests' }, { label: t('Merged pull requests'), key: 'mergedPullRequests' }, + { key: 'ci', label: t('CI red share'), today: share(today.ci), week: share(week.ci) }, { label: t('Median issue close'), key: 'medianIssueCloseMs', format: duration }, { label: t('Median PR merge'), key: 'medianPullMergeMs', format: duration }, ].map(row => { + // The CI row already carries its own formatted share; every other row reads its number from + // today/week by key. + if (row.today !== undefined) return { key: row.key, label: row.label, today: row.today, week: row.week } const format = row.format ?? count - return { label: row.label, today: format(today[row.key]), week: format(week[row.key]) } + // `key` travels with the row so the chart finds its own series by name, not by position. + return { key: row.key, label: row.label, today: format(today[row.key]), week: format(week[row.key]) } }) return { dayStart: snapshot?.dayStart ?? null, @@ -266,17 +276,13 @@ function Diagnose({ onProbe }) { export function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, onProbe = null }) { const payload = view ?? snapshot if (!payload) return null - const { rows, ci, open } = metricRows(payload.metrics ?? payload) + const metricsPayload = payload.metrics ?? payload + const { rows, ci, open } = metricRows(metricsPayload) const error = payload.error ?? null const state = payload.state ?? 'fresh' return h('header', { className: 'bt-del-head' }, h('div', { className: 'bt-del-metrics' }, - h('table', { className: 'bt-del-table' }, - h('thead', null, h('tr', null, h('th', null, ''), h('th', null, t('Today')), h('th', null, t('7 days')))), - h('tbody', null, rows.map(row => h('tr', { key: row.label }, - h('th', { scope: 'row' }, row.label), - h('td', null, row.today), - h('td', null, row.week))))), + h(DeliveryMetrics, { rows, daily: metricsPayload.daily }), // An unreadable CI number is "unknown", never "0 of 0" (#1211). h('p', { className: 'bt-del-ci', 'data-ci': ci === null ? 'unknown' : 'read' }, ci === null ? t('CI not available this time') : t('CI failed {failed} of {total}', { failed: ci.failed, total: ci.total }), @@ -289,6 +295,7 @@ export function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, on ? ` · ${t('median open age {age}', { age: duration(open.medianIssueAgeMs) })}` : '')), h('p', { className: 'bt-del-asof' }, state === 'loading' ? loadingText(payload.elapsedMs) : t('as of {time}', { time: timeOf(payload.asOf) }), + state !== 'loading' && todayPartial(metricsPayload.daily) ? ` · ${t('today is partial · as of {time}', { time: clockOf(payload.asOf) })}` : '', payload.refreshing ? ` · ${t('refreshing…')}` : '', error ? ` · ${hintFor(error, { repo })}` : '', onRetry ? h('button', { type: 'button', className: 'bt-soft bt-soft-sm bt-del-retry', onClick: onRetry }, t('Retry')) : null, @@ -428,4 +435,4 @@ export const DELIVERY_CSS = ` .bt-del-columns{flex-direction:column} .bt-del-column{min-width:0} } -` +` + DELIVERY_CHART_CSS diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js index e0ba5497..a0016beb 100644 --- a/core/src/client/i18n.js +++ b/core/src/client/i18n.js @@ -1326,6 +1326,16 @@ const HOST_ZH = { 'median open age {age}': '开着的单中位 {age}', 'as of {time}': '截至 {time}', 'refreshing…': '刷新中…', + // #1212: the 7-day lines. A day the host could not read, and a snapshot with no series at all. + 'not available': '未取到', + 'No 7-day data yet.': '7 天数据未取到', + '{label} over the last 7 days': '{label}近 7 天', + 'Last 7 days': '近 7 天', + 'so far': '截至现在', + 'today is partial · as of {time}': '今天为部分日 · 截至 {time}', + '* {count} of 7 days had data': '* 7 天中 {count} 天有数', + '* {count} of {days} days had data': '* {days} 天中 {count} 天有数', + 'CI red share': 'CI 红占比', 'No snapshot yet.': '还没有快照。', 'Nothing here.': '这里还没有。', 'First load in progress, {seconds} s elapsed': '首次加载中,已 {seconds} 秒', diff --git a/core/test/delivery-chart.test.js b/core/test/delivery-chart.test.js new file mode 100644 index 00000000..013f8ee8 --- /dev/null +++ b/core/test/delivery-chart.test.js @@ -0,0 +1,236 @@ +// The delivery trend chart (#1212): one x per day, a line that breaks at an unknown day instead of +// drawing through zero, a real zero as a filled dot on the baseline, today hollow only while it is +// still running, and no SVG at all when the snapshot carries no series. The chart is pure, so the +// geometry is asserted without a browser; the component tree is read the way delivery-view.test.js +// reads its view. +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' + +const bundle = await build({ + entryPoints: [fileURLToPath(new URL('../src/client/delivery-chart.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise, Number, Math, String, Array, Object, + require: (name) => { + if (name !== 'react') throw new Error(`no platform module ${name}`) + return { + createElement: (type, props, ...children) => { + const kids = children.flat(Infinity) + if (typeof type === 'function') return type({ ...(props ?? {}), children: kids }) + return { type, props: props ?? {}, children: kids } + }, + } + }, +}) +const { + CHART_WIDTH, CHART_HEIGHT, CHART_DAYS, CHART_KEYS, chartDays, chartGeometry, dayValue, + dayLabel, pointTitle, knownDays, todayPartial, DeliveryChart, DeliveryMetrics, DELIVERY_CHART_CSS, +} = module.exports + +const walk = (node, out = []) => { + if (node === null || typeof node !== 'object') return out + out.push(node) + for (const child of node.children ?? []) walk(child, out) + return out +} +const textOf = (node) => { + if (node === null || node === undefined || typeof node !== 'object') return String(node ?? '') + return [node.props?.title ?? '', ...(node.children ?? []).map(textOf)].join(' ') +} + +// Seven Asia/Shanghai days, 2026-10-20 … 2026-10-26; the last one is today and still running. The +// contract the host half is written against: { date, partial?, , ci: { failed, counted, total } }. +const daily = [ + { date: '2026-10-20', newIssues: 2, closedIssues: 1, newPullRequests: 3, mergedPullRequests: 0, ci: { failed: 1, total: 10, counted: 10 } }, + { date: '2026-10-21', newIssues: 4, closedIssues: 2, newPullRequests: 1, mergedPullRequests: 2, ci: { failed: 0, total: 8, counted: 8 } }, + { date: '2026-10-22', newIssues: 1, closedIssues: 3, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 3, total: 9, counted: 9 } }, + { date: '2026-10-23', newIssues: 6, closedIssues: 0, newPullRequests: 0, mergedPullRequests: 4, ci: { failed: 0, total: 0, counted: 0 } }, + { date: '2026-10-24', newIssues: 3, closedIssues: 5, newPullRequests: 4, mergedPullRequests: 2, ci: { failed: 2, total: 5, counted: 4 } }, + { date: '2026-10-25', newIssues: 0, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 3, ci: { failed: 1, total: 6, counted: 6 } }, + { date: '2026-10-26', newIssues: 5, closedIssues: 2, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 1, total: 4, counted: 4 }, partial: true }, +] + +test('#1212: seven days, the rightmost is today, and a longer series keeps its end', () => { + assert.equal(CHART_DAYS, 7) + const days = chartDays(daily) + assert.equal(days.length, 7) + assert.equal(dayLabel(days[6]), '2026-10-26', 'the contract carries the date itself') + assert.equal(dayLabel({ start: '2026-10-25T16:00:00.000Z' }), '2026-10-26', 'an older ISO start still labels a day') + assert.equal(chartDays(undefined), null, 'no series is null, not seven zeros') + assert.equal(chartDays([]), null) + assert.equal(chartDays([...daily, { date: '2026-10-27', newIssues: 9 }]).length, 7) + assert.equal(todayPartial(daily), true) + assert.equal(todayPartial(daily.map(day => ({ ...day, partial: undefined }))), false) +}) + +test('#1212: 168×28, one point per day, x across the box, the largest day at the top', () => { + assert.equal(CHART_WIDTH, 168) + assert.equal(CHART_HEIGHT, 28) + const geo = chartGeometry(daily, 'newIssues') + assert.equal(geo.width, CHART_WIDTH) + assert.equal(geo.height, CHART_HEIGHT) + assert.equal(geo.dots.length, 7) + const xs = geo.dots.map(dot => dot.x) + assert.deepEqual(xs, [...xs].sort((a, b) => a - b), 'x rises with the day') + assert.equal(xs[0], 3) + assert.equal(xs[6], CHART_WIDTH - 3) + assert.equal(Math.min(...geo.dots.map(dot => dot.y)), 3, 'the largest value sits on the top padding') + assert.equal(Math.max(...geo.dots.map(dot => dot.y)), CHART_HEIGHT - 3, 'a zero day sits on the bottom padding') + assert.equal(geo.segments.length, 1) + assert.equal(geo.dots[6].title, '2026-10-26 · 5') +}) + +test('#1212: an unknown day breaks the line instead of drawing through zero', () => { + const gapped = daily.map((item, i) => (i === 3 ? { ...item, newIssues: null } : item)) + const geo = chartGeometry(gapped, 'newIssues') + assert.equal(geo.dots[3].state, 'missing', 'the day is known to be missing') + assert.equal(geo.dots[3].y, null, 'and has no point') + assert.equal(geo.known, 6) + assert.equal(geo.segments.length, 2) + assert.equal(geo.segments[0].d.split('L').length, 3, 'first stretch: three days') + assert.equal(geo.segments[1].d.split('L').length, 3, 'second stretch: the three days after the gap') + assert.equal(geo.dots[3].title, '2026-10-23 · not available') + // A real zero connects and keeps a filled dot on the baseline: never the same picture as a gap. + const zeroed = daily.map((item, i) => (i === 3 ? { ...item, newIssues: 0 } : item)) + const flat = chartGeometry(zeroed, 'newIssues') + assert.equal(flat.segments.length, 1, 'a real zero still connects') + assert.equal(flat.dots[3].state, 'full') + assert.equal(flat.dots[3].y, CHART_HEIGHT - 3, 'the zero dot sits on the baseline') +}) + +test('#1212: today is hollow and dashed only while it is still running', () => { + const partial = chartGeometry(daily, 'newIssues') + assert.equal(partial.dots[6].state, 'partial') + assert.equal(partial.dots[6].hollow, true) + assert.equal(partial.segments[0].dashed, true) + assert.equal(partial.dots.filter(dot => dot.state === 'partial').length, 1, 'only today is partial') + const finished = chartGeometry(daily.map(day => ({ ...day, partial: undefined })), 'newIssues') + assert.equal(finished.dots[6].state, 'full', 'a finished day is a filled dot') + assert.equal(finished.dots[6].hollow, false) + assert.equal(finished.segments[0].dashed, false, 'and a solid line') +}) + +test('#1212: the CI metric is the red share of the runs that counted', () => { + assert.equal(dayValue(daily[0], 'ci'), 0.1) + assert.equal(dayValue(daily[4], 'ci'), 0.5, 'counted 4 of 5 runs: 2/4') + assert.equal(dayValue(daily[3], 'ci'), null, 'no runs that day is unknown, not 0%') + const geo = chartGeometry(daily, 'ci') + assert.equal(geo.dots[3].state, 'missing') + assert.equal(geo.dots[0].title, '2026-10-20 · 10%') + assert.equal(knownDays(daily, 'ci'), 6) +}) + +test('#1212: the component draws a 168×28 svg with a state and a title on every point', () => { + const node = DeliveryChart({ daily, metricKey: 'newIssues', label: 'New issues' }) + assert.equal(node.type, 'svg') + assert.equal(node.props.width, 168) + assert.equal(node.props.height, 28) + assert.equal(node.props.viewBox, '0 0 168 28') + const all = walk(node) + const circles = all.filter(item => item.type === 'circle') + assert.equal(circles.length, 7) + assert.equal(circles[6].props['data-state'], 'partial') + assert.equal(circles[6].props['data-hollow'], 'true') + assert.equal(circles[6].props.r, 2.5, 'the hollow today dot is drawn larger') + assert.equal(circles[0].props.r, 2, 'a measured day keeps the small dot') + assert.equal(circles[6].props.fill, undefined, 'hollow comes from CSS, not a hard-coded colour') + assert.equal(all.find(item => item.type === 'path').props['stroke-dasharray'], '2 2') + const titles = all.filter(item => item.type === 'title').map(item => item.children.join('')) + assert.equal(titles[6], '2026-10-26 · 5') + assert.equal(titles[3], '2026-10-23 · 6') + // A missing day is not a dot at all, so no circle can stand in for it. + const gapped = walk(DeliveryChart({ daily: daily.map((item, i) => (i === 3 ? { ...item, newIssues: null } : item)), metricKey: 'newIssues' })) + const gappedCircles = gapped.filter(item => item.type === 'circle') + assert.equal(gappedCircles.length, 6) + assert.equal(gappedCircles.some(circle => circle.props['data-state'] === 'missing'), false) + assert.equal(gappedCircles.filter(circle => circle.props['data-state'] === 'full').length, 5) +}) + +test('#1212: a metric with no readable day draws nothing', () => { + assert.equal(pointTitle({ date: '2026-10-23' }, 'newIssues'), '2026-10-23 · not available') + assert.equal(DeliveryChart({ daily: daily.map(item => ({ ...item, mergedPullRequests: null })), metricKey: 'mergedPullRequests' }), null) +}) + +test('#1212: one grey line instead of an empty chart when the snapshot has no series', () => { + const rows = CHART_KEYS.map(key => ({ key, label: key, today: '4', week: '30' })) + for (const missing of [undefined, null, [], 'nope']) { + const all = walk(DeliveryMetrics({ rows, daily: missing })) + assert.equal(all.some(item => item.type === 'svg'), false, 'no svg without data') + assert.equal(all.some(item => item.type === 'circle'), false) + const empty = all.find(item => item.props?.['data-flybot'] === 'delivery-chart-empty') + assert.ok(empty, 'the empty state is drawn') + assert.match(textOf(empty), /No 7-day data yet\.|7 天数据未取到/) + assert.equal(all.filter(item => item.props?.['data-metric'] && item.type === 'div').length, 5) + } +}) + +test('#1212: five lines, Σ totals, medians with no line and no total, 900px breakpoint', () => { + const rows = [ + { key: 'newIssues', label: 'New issues', today: '5', week: '21' }, + { key: 'closedIssues', label: 'Closed issues', today: '2', week: '14' }, + { key: 'newPullRequests', label: 'New pull requests', today: '2', week: '13' }, + { key: 'mergedPullRequests', label: 'Merged pull requests', today: '1', week: '13' }, + { key: 'ci', label: 'CI red share', today: '25%', week: '18%' }, + { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }, + { key: 'medianPullMergeMs', label: 'Median PR merge', today: '3h', week: '25h' }, + ] + const all = walk(DeliveryMetrics({ rows, daily })) + const rowNodes = all.filter(item => item.props?.className === 'bt-del-dm-row') + assert.equal(rowNodes.length, 7) + assert.equal(all.filter(item => item.type === 'svg').length, 5, 'the five countable metrics carry a line') + // The two median rows keep their numbers and get no line, no placeholder cell and no Σ. + const median = rowNodes[5] + assert.equal(median.children.length, 3, 'label, today, 7 days — no chart cell at all') + assert.equal(median.children.some(child => child.props.className === 'bt-del-dm-chart'), false) + assert.equal(median.children[2].children.join(''), '2h', 'a median is not summed') + assert.equal(rowNodes[6].children[2].children.join(''), '25h') + // One primary number per row: today is bold ink, the total is muted and prefixed. + assert.equal(rowNodes[0].children[2].props.className, 'bt-del-dm-value bt-del-dm-today') + assert.equal(rowNodes[0].children[3].props.className, 'bt-del-dm-value bt-del-dm-week') + assert.equal(rowNodes[0].children[3].children.join(''), 'Σ 21') + // A day the host could not read makes the total incomplete: it wears the star. + const gapped = walk(DeliveryMetrics({ rows, daily: daily.map((item, i) => (i === 2 ? { ...item, newIssues: null } : item)) })) + const gappedRows = gapped.filter(item => item.props?.className === 'bt-del-dm-row') + assert.equal(gappedRows[0].children[3].children.join(''), 'Σ 21*') + const foot = gapped.find(item => item.props?.['data-flybot'] === 'delivery-chart-foot') + assert.ok(foot, 'the star is explained') + assert.match(textOf(foot), /\* [0-9]+ of [0-9]+ days|天中/) + // Headers: the two value columns say today and the 7-day total; today names itself as partial. + const head = all.find(item => item.props?.['data-flybot'] === 'delivery-metrics-head') + const labels = head.children.map(child => (child.children ?? []).join('')) + // The partial day is named once, on the as-of line; the shared header keeps one word. + assert.equal(labels[2], 'Today') + assert.equal(labels[3], '7 days', 'no Σ in the shared header') + assert.match(DELIVERY_CHART_CSS, /max-width:899\.98px/, 'the line moves under its row below 900px') + assert.match(DELIVERY_CHART_CSS, /\.bt-del-dm-chart\{order:5;flex:0 0 100%/, 'the line takes its own line there') + assert.match(DELIVERY_CHART_CSS, /min-height:44px/, 'the row grows to 44px there') + assert.match(DELIVERY_CHART_CSS, /flex:0 0 168px/, 'the line does not shrink') + assert.match(DELIVERY_CHART_CSS, /stroke-width:1\.5/) + assert.match(DELIVERY_CHART_CSS, /fill:var\(--bt-accent\)/) + assert.equal(/#[0-9a-fA-F]{3,8}\b/.test(DELIVERY_CHART_CSS), false, 'no hard-coded colour') +}) + +test('#1212: a row finds its series by key, not by its position', () => { + const rows = [ + { key: 'ci', label: 'CI red share', today: '25%', week: '18%' }, + { key: 'newIssues', label: 'New issues', today: '5', week: '21' }, + { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }, + ] + const all = walk(DeliveryMetrics({ rows, daily })) + const rowNodes = all.filter(item => item.props?.className === 'bt-del-dm-row') + assert.deepEqual(rowNodes.map(node => node.props['data-metric']), ['ci', 'newIssues', undefined]) + assert.equal(rowNodes[0].children.length, 4, 'the countable row keeps its line') + assert.equal(rowNodes[0].children[2].children.join(''), '25%') + // A ratio is not a sum: the CI row keeps its number plain, while a count wears the star. + assert.equal(rowNodes[0].children[3].children.join(''), '18%', 'the CI share is a ratio, never summed') + assert.equal(rowNodes[1].children[3].children.join(''), 'Σ 21', 'the count keeps its Σ') + assert.equal(rowNodes[2].children.length, 3, 'a median row stays without a line, wherever it sits') + const svgs = all.filter(item => item.type === 'svg') + assert.equal(svgs.length, 2) +}) diff --git a/core/test/delivery-view.test.js b/core/test/delivery-view.test.js index 513b0285..cbc9cab9 100644 --- a/core/test/delivery-view.test.js +++ b/core/test/delivery-view.test.js @@ -161,9 +161,16 @@ test('five delivery columns, localized labels, badge wording (#1173)', () => { test('the header carries today, the 7-day comparison, the CI caveat and open ages (#1173)', () => { const rows = metricRows(snapshot.metrics) - assert.equal(rows.rows.length, 6) - assert.deepEqual({ ...rows.rows[0] }, { label: 'New issues', today: '4', week: '30' }) - assert.deepEqual({ ...rows.rows[4] }, { label: 'Median issue close', today: '1h', week: '2h' }) + // #1212 adds the CI red share as a seventh row: it draws a line like the counts, but as a ratio + // it never carries a Σ total. + assert.equal(rows.rows.length, 7) + // #1212: every row carries the metric key the chart looks its series up by, so the expectation + // names all six — the shape change is deliberate, not something to hide from the assertion. + assert.deepEqual({ ...rows.rows[0] }, { key: 'newIssues', label: 'New issues', today: '4', week: '30' }) + assert.deepEqual({ ...rows.rows[4] }, { key: 'ci', label: 'CI red share', today: '0%', week: '100%' }) + assert.deepEqual({ ...rows.rows[5] }, { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }) + assert.deepEqual([...rows.rows.map(row => row.key)], + ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci', 'medianIssueCloseMs', 'medianPullMergeMs']) assert.equal(rows.ci.failed, 2) assert.equal(rows.ci.total, 1200) assert.equal(rows.ci.caveat, true) @@ -171,6 +178,35 @@ test('the header carries today, the 7-day comparison, the CI caveat and open age assert.equal(rows.open.needsHuman, 4) }) +test('the CI red share reaches the page as a line, and the medians draw none (#1212)', () => { + // A key the table has no row for would draw no line: the CI share has to be a row, not a key only + // the chart knows about. + const daily = [ + { date: '2026-10-19', newIssues: 1, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 0, ci: { failed: 0, counted: 10 } }, + { date: '2026-10-20', newIssues: 2, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 1, ci: { failed: 2, counted: 10 } }, + { date: '2026-10-21', newIssues: 0, closedIssues: 3, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 1, counted: 4 } }, + { date: '2026-10-22', newIssues: 4, closedIssues: 2, newPullRequests: 0, mergedPullRequests: 2, ci: { failed: null, counted: null } }, + { date: '2026-10-23', newIssues: 3, closedIssues: 3, newPullRequests: 1, mergedPullRequests: 1, ci: { failed: 0, counted: 8 } }, + { date: '2026-10-24', newIssues: 5, closedIssues: 4, newPullRequests: 3, mergedPullRequests: 2, ci: { failed: 3, counted: 12 } }, + { date: '2026-10-25', partial: true, newIssues: 4, closedIssues: 2, newPullRequests: 5, mergedPullRequests: 3, ci: { failed: 2, counted: 10 } }, + ] + const board = DeliveryBoard({ snapshot: { ...snapshot, metrics: { ...snapshot.metrics, daily } }, message: '' }) + const charts = walk(board).filter(node => node.props?.className === 'bt-del-chart') + assert.deepEqual([...charts.map(node => node.props['data-metric'])], + ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci']) + const rows = walk(board).filter(node => node.props?.className === 'bt-del-dm-row') + const ciRow = rows.find(node => node.props['data-metric'] === 'ci') + assert.ok(ciRow, 'the CI share is a row of the table, not only a chart key') + // A share of the week, never a sum. + assert.match(text(ciRow), /100%/) + assert.equal(text(ciRow).includes('Σ'), false) + // Medians cannot be added up and get no chart at all. + for (const key of ['medianIssueCloseMs', 'medianPullMergeMs']) { + const row = rows.find(node => node.props['data-metric'] === key) + assert.equal(walk(row).some(node => node.props?.className === 'bt-del-chart'), false) + } +}) + test('the board renders all five columns and a card with labels, owner and link (#1173)', () => { const board = DeliveryBoard({ snapshot: { ...snapshot, columns: snapshot.columns }, message: '' }) const sections = walk(board).filter(node => node.type === 'section' && node.props?.['data-column']) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index d9548595..0021434d 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -39,6 +39,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl | `core/src/client/inbox.js` | Agent inbox, step 1 (#349): the **Inbox** row at the top of the sidebar (`InboxRow`, count badge = cards in *Needs you*) and the inbox dialog (`InboxDialog`, `ui.inbox`, `actions.openInbox` / `closeInbox`) with the groups **Needs you**, **In progress** and **Done** (`inboxModel`); failed turns, a *Latest* line and a per-card `N tool calls` count (#471); ask_user cards (a group's and the Bot's own chat) answered in the inbox (#552, #1140); a *Needs you* title and reason wrap in full so the ask can be read on the card (#1177); a result card accepted (**Accept**) or sent back with a note (**Send back** → the box → **Send it back**), with its `result.links` evidence and the producing Bot's model, and a sent-back card reading *Sent back: …* (#401). Tests: `core/test/inbox.test.js`. | | `core/src/client/turn-failures.js` | Dead-turn lines (#1044): a turn that died with no text draws one line above the composer (`flybot.session.dock`, `data-flybot=turn-failures`), newest last, at most 3, naming the provider error from the host's `ended` entry and the streak when it is above 1. Registered by `index.js` (`flybot-turn-failures`, order 20); `failureLines(snapshot, place, t)` lives in `core/src/shared/turn-failures.js` so the test reads it without React. It never posts, retries or counts as a reply. Tests: `core/test/dead-turn.test.js`. | | `core/src/client/delivery-view.js` | The **Delivery** view of the task dialog (#1173, #1211): five columns (`raw`, `ready`, `working`, `review`, `done-today`) from one host snapshot, cards that link out, the verdict/CI badge, and the header numbers. A read is one of four states and each reads differently, never as a blank board: `loading` counts its own seconds (“首次加载中,已 N 秒”) and polls every 2 s, `fresh`/`stale` poll every 60 s and a stale board keeps its columns with “显示 03:12 的快照 · 刷新失败:…” beside the header, and `failed` names the endpoint, the advice and a **Retry** button whose countdown really counts: the screen redraws once a second while a retry is scheduled and not otherwise, and the rate-limit and timeout advice says `while this page is open it retries on its own`, because the host installs no timer (#1227). “还没有快照。” is reachable only from `idle`, a host that never tried. The header's **Diagnose** button prints the five `probeGitHub()` lines into a copyable block, so the user reads the same statuses `core/scripts/delivery-probe.mjs` prints. Read-only: it renders, it never writes. Tests: `core/test/delivery-view.test.js`, `core/test/kanban.test.js`. | +| `core/src/client/delivery-chart.js` | The 7-day line each delivery metric draws (#1212): `chartGeometry` is pure — seven x positions, a y per known day (`span = max(known)`, baseline 0), the path split at every day with no number, so a gap can never read as zero. A day without a number draws no dot at all; the `full` / `partial` split is a geometry state, not an attribute for the missing day. `DeliveryChart` renders one 168×28 SVG per metric with a 1.5px stroke, `r=2` dots (`r=2.5` for the one hollow dot) and a `title` on every point (`2026-10-26 · 12`, `2026-10-26 · 未取到`); each drawn dot carries `data-state` `full` or `partial`, a real `0` is a filled dot on the baseline, and today with `partial: true` is hollow (`fill:none`, `stroke-width:1.5`) with a `2 2` dashed stretch. `DeliveryMetrics` is the whole block the view calls once — one row per metric, `label | line | Today | 7 days`, five lines (the four counts plus the CI share), with `Σ` only on the counting rows (a ratio and a median are not sums) and a star plus a one-line note, naming the window length, when some days were not read. A snapshot with no `metrics.daily` draws no SVG at all and says `7 天数据未取到` in one line. Colours come from `--bt-accent` / `--bt-ink-*` only. Tests: `core/test/delivery-chart.test.js`. | | `core/src/client/kanban.js` | Per-Bot task board (#1058, #1099): the **Tasks** row under Inbox (`KanbanRow`, `ui.board`, `actions.openBoard` / `closeBoard`) and `KanbanDialog`. `boardRail` lists the viewer's Bots nested by `createdBy` and the groups they administer; `boardColumns` maps task status onto **Not started** / **In progress** / **Done**. Cards show id, priority, title, note, assignee and progress. The dialog has two tabs (#1173), **Tasks** and **Delivery**; the Delivery view is repository-wide, so its Bot rail is not rendered and it cannot be pointed at one Bot. In a group scope, `columnTree` nests subtasks under the overall task (`kind: 'group'`) when they share a column, and a subtask in another column says which overall task it is part of. That scope also says assigning, claiming, and progress live on the board (overall task, subtask, assignee, column, progress) and that @ only wakes someone. Drag or **Start** / **Complete** calls `actions.taskUpdate`; only the Bot's owner gets those actions. A group scope loads `tasks { roomId }`. Tests: `core/test/kanban.test.js`. | | `core/src/client/standing-approvals.js` | Standing tool approvals (#540): `StandingApprovalsCard`, the **Always allowed** card in Bot settings (the Bot's grants) and Group settings (the grants this person gave in the group), one row per kind with **Remove** (`actions.removeStandingApproval`). The approval cards (`member-status.js` `ApprovalCard`, the inbox card) add **Always allow for this Bot** and, for an ask from a group, **Always allow for this group** (`alwaysChoices`; never on a friend's Bot). Host side in [core-host](core-host.md#standing-tool-approvals-540). Tests: `core/test/member-status.test.js`, `core/test/standing-approvals.test.js`. | | `core/src/client/mobile.js` | Phone-width layout, step 1 (#362): `MOBILE_CSS` under `@media (max-width:640px)` for held-reply and floating approval buttons, the chat pointers and Agent inbox cards. Tests: `core/test/mobile-layout.test.js`. | diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index d668b15a..71d62d37 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -1184,7 +1184,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'The admin must be a member of the group': '管理员必须是群成员', 'Unknown group chat': '找不到这个群聊', 'Hand the admin role to another member instead of leaving the group without one': '请把管理员交给其他成员,不要让群聊没有管理员', -@@ -905,6 +905,70 @@ const HOST_ZH = { +@@ -905,6 +905,80 @@ const HOST_ZH = { 'Write the entry to save.': '请写下要保存的内容。', 'This DeepSeek Harness has no folder chooser': '这个 DeepSeek Harness 没有文件夹选择窗口', 'This DeepSeek Harness cannot list folders': '这个 DeepSeek Harness 不能列出文件夹', @@ -1222,6 +1222,16 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js + 'median open age {age}': '开着的单中位 {age}', + 'as of {time}': '截至 {time}', + 'refreshing…': '刷新中…', ++ // #1212: the 7-day lines. A day the host could not read, and a snapshot with no series at all. ++ 'not available': '未取到', ++ 'No 7-day data yet.': '7 天数据未取到', ++ '{label} over the last 7 days': '{label}近 7 天', ++ 'Last 7 days': '近 7 天', ++ 'so far': '截至现在', ++ 'today is partial · as of {time}': '今天为部分日 · 截至 {time}', ++ '* {count} of 7 days had data': '* 7 天中 {count} 天有数', ++ '* {count} of {days} days had data': '* {days} 天中 {count} 天有数', ++ 'CI red share': 'CI 红占比', + 'No snapshot yet.': '还没有快照。', + 'Nothing here.': '这里还没有。', + 'First load in progress, {seconds} s elapsed': '首次加载中,已 {seconds} 秒', @@ -3024,16 +3034,15 @@ diff --git a/src/host/connectors.js b/src/host/connectors.js const token = typeof body.token === 'string' ? body.token.trim() : '' - if (token === '') throw new Error('Paste a token first') - if (insecure) throw new Error(INSECURE) -- await rt.loadSecrets() ++ if (token === '') throw new Error(c.def.id === GITHUB ? 'Paste a token first' : 'Paste a key first') ++ if (c.insecure) throw new Error(INSECURE(c.def.name)) + await rt.loadSecrets() - const kept = rt.secretValue(GITHUB_SECRET) !== undefined - const { login } = await signIn(token, { api: options.api }) - await rt.secretEndpoint('secret-set', { name: GITHUB_SECRET, value: token, purpose: 'GitHub connector', ...(kept ? {} : { scope: [] }) }) - state.connectors = { ...state.connectors, [GITHUB]: { login, connectedAt: Date.now() } } - await rt.save() - await sync() -+ if (token === '') throw new Error(c.def.id === GITHUB ? 'Paste a token first' : 'Paste a key first') -+ if (c.insecure) throw new Error(INSECURE(c.def.name)) -+ await rt.loadSecrets() + const oldToken = rt.secretValue(c.def.secret) + const previous = saved(c) + const record = await c.def.check({ token, options: c.options, saved: previous, body }) @@ -3050,8 +3059,8 @@ diff --git a/src/host/connectors.js b/src/host/connectors.js + } + await retire(c, { gone: previous?.session, kept: record.session, keys: [oldToken, token] }) + await sync(c) -+ return connectorsView() -+ }, + return connectorsView() + }, + // Changes which apps a connected gateway offers, without asking for the key again. + async 'connector-apps'(body) { + const c = known(body.id) @@ -3070,8 +3079,8 @@ diff --git a/src/host/connectors.js b/src/host/connectors.js + } + await retire(c, { gone: previous?.session, kept: record.session, keys: [token] }) + await sync(c) - return connectorsView() - }, ++ return connectorsView() ++ }, + // Searches Composio's app list: with the saved key, or before connecting with the key being pasted + // (sent only to Composio, never saved by this call). + async 'connector-app-search'(body) { @@ -4340,8 +4349,7 @@ diff --git a/src/host/schedules.js b/src/host/schedules.js - tasks.push({ ...entry, ...owner }) + const lastRun = lastRunOf(entry) + tasks.push({ ...entry, ...owner, ...(lastRun ? { lastRun } : {}) }) - } -- return { available: true, tasks } ++ } + for (const entry of Object.values(paused())) { + const owner = ownerOf(entry.sessionId) + if (!known(owner)) continue @@ -4462,7 +4470,8 @@ diff --git a/src/host/schedules.js b/src/host/schedules.js + void rt.save().catch(() => {}) + } + throw error -+ } + } +- return { available: true, tasks } + return schedulesView() + } + @@ -5934,7 +5943,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js test('signIn asks GitHub who owns the token', async () => { const token = fresh() -@@ -41,8 +41,7 @@ +@@ -41,8 +41,7 @@ const teamHome = () => { } // A loader that hands out a fake MCP client and records each mount. @@ -5944,7 +5953,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js const mounts = [] const plugin = { name: 'mcp-client', apply() {} } const loader = { -@@ -50,17 +50,21 @@ +@@ -50,17 +50,21 @@ function setup({ fail = false, github = { api: 'https://gh.test' } } = {}) { unwrapExports: exports => exports.default, } const ctx = fakeContext({ services: { loader } }) @@ -5968,7 +5977,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js return { ctx, home, mounts } } -@@ -75,20 +75,20 @@ +@@ -75,20 +75,20 @@ async function call(ctx, endpoint, payload) { return response.json() } @@ -5993,7 +6002,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js assert.equal(mounts.length, 1) assert.deepEqual(mounts[0].config, { transport: 'streamable-http', -@@ -98,12 +98,12 @@ +@@ -98,12 +98,12 @@ test('connecting saves the token, kept out of every Bot shell, and mounts GitHub failOnStartupError: true, }) const state = await call(ctx, 'state', {}) @@ -6008,7 +6017,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js assert.equal(mounts[0].disposed, true) assert.deepEqual((await call(ctx, 'state', {})).value.secrets, []) }) -@@ -113,7 +113,7 @@ +@@ -113,7 +113,7 @@ test('a token GitHub refuses is not saved', async (t) => { t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(401, {}) const { ctx, home, mounts } = setup() @@ -6017,7 +6026,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js const result = await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) assert.equal(result.ok, false) assert.match(result.error.message, /did not accept/) -@@ -126,10 +126,10 @@ +@@ -126,10 +126,10 @@ test('a failed MCP connection shows the error without the token, and Try again m t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) const { ctx, home, mounts } = setup({ fail: true }) @@ -6030,7 +6039,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js assert.equal(github.state, 'error') assert.ok(!github.error.includes(token), github.error) assert.match(github.error, /\[secret:GITHUB_TOKEN\]/) -@@ -142,7 +142,7 @@ +@@ -142,7 +142,7 @@ test('deleting GITHUB_TOKEN on the Secrets page unmounts GitHub', async (t) => { t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) const { ctx, home, mounts } = setup() @@ -6039,7 +6048,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) await call(ctx, 'secret-delete', { name: 'GITHUB_TOKEN' }) const state = await call(ctx, 'state', {}) -@@ -150,13 +150,19 @@ +@@ -150,13 +150,19 @@ test('deleting GITHUB_TOKEN on the Secrets page unmounts GitHub', async (t) => { assert.equal(state.value.connectors[0].state, 'off') }) @@ -6062,7 +6071,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js }) test('a GitHub address without https refuses to send the token', async (t) => { -@@ -165,10 +165,603 @@ +@@ -165,10 +165,603 @@ test('a GitHub address without https refuses to send the token', async (t) => { let asked = false globalThis.fetch = async () => { asked = true; return answer(200, { login: 'octo' }) } const { ctx, home, mounts } = setup({ github: { url: 'http://mcp.test/', api: 'https://gh.test' } }) @@ -6758,7 +6767,7 @@ diff --git a/test/follow-through.test.js b/test/follow-through.test.js + t.mock.timers.enable({ apis: ['setTimeout'] }) + mockedTimers = t.mock.timers + t.after(() => { if (mockedTimers === t.mock.timers) mockedTimers = undefined }) - } ++} +// How long a wait may take on the real clock, and how finely it steps. +const REAL_WAIT_MS = 5000 +const REAL_WAIT_STEP_MS = 5 @@ -6788,7 +6797,7 @@ diff --git a/test/follow-through.test.js b/test/follow-through.test.js + await new Promise(resolve => setImmediate(resolve)) + } + return check() -+} + } +const settle = async (check) => { assert.ok(await waitFor(check), 'condition not reached') } +// Writes to `inflight.json` land on a later tick; after a mock-timer tick and after `stop()` the file +// can still hold the previous write. Wait for them rather than reading early (the note of a failed @@ -7396,7 +7405,7 @@ diff --git a/test/history.test.js b/test/history.test.js diff --git a/test/home-lock.test.js b/test/home-lock.test.js --- a/test/home-lock.test.js +++ b/test/home-lock.test.js -@@ -5,7 +5,7 @@ +@@ -5,7 +5,7 @@ import { syncBuiltinESMExports } from 'node:module' import { hostname, tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' @@ -7405,7 +7414,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js import { homeLock } from '../src/host/home-lock.js' import { fakeContext } from './fake-ctx.js' -@@ -118,10 +118,14 @@ +@@ -118,10 +118,14 @@ function start(t, home) { route = options return register(options) } @@ -7421,7 +7430,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js return call } -@@ -185,3 +185,135 @@ +@@ -185,3 +185,135 @@ test('a reading host follows the other host\'s keys, never writes them, and keep const saved = JSON.parse(readFileSync(secretsPath, 'utf8')).secrets.map(secret => secret.name).sort() assert.deepEqual(saved, ['FIRST_KEY', 'MINE', 'SECOND_KEY', 'THIRD_KEY']) }) @@ -7557,7 +7566,6 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js + assert.equal((await call('set-prefs', { theme: 'dark' })).error.code, 'bot/read-only') + assert.equal((await call('secret-set', { name: 'MINE', value: 'mine-value-1' })).error.code, 'bot/read-only') +}) - diff --git a/test/images.test.js b/test/images.test.js --- a/test/images.test.js +++ b/test/images.test.js diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index b8fee0a5..470f1e1b 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -720,7 +720,7 @@ diff --git a/client.js b/client.js "The admin must be a member of the group": "管理员必须是群成员", "Unknown group chat": "找不到这个群聊", "Hand the admin role to another member instead of leaving the group without one": "请把管理员交给其他成员,不要让群聊没有管理员", -@@ -951,7 +951,71 @@ var HOST_ZH = { +@@ -951,7 +951,81 @@ var HOST_ZH = { "The model wrote nothing": "模型没有输出内容", "Write the entry to save.": "请写下要保存的内容。", "This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口", @@ -760,6 +760,16 @@ diff --git a/client.js b/client.js + "median open age {age}": "开着的单中位 {age}", + "as of {time}": "截至 {time}", + "refreshing…": "刷新中…", ++ // #1212: the 7-day lines. A day the host could not read, and a snapshot with no series at all. ++ "not available": "未取到", ++ "No 7-day data yet.": "7 天数据未取到", ++ "{label} over the last 7 days": "{label}近 7 天", ++ "Last 7 days": "近 7 天", ++ "so far": "截至现在", ++ "today is partial · as of {time}": "今天为部分日 · 截至 {time}", ++ "* {count} of 7 days had data": "* 7 天中 {count} 天有数", ++ "* {count} of {days} days had data": "* {days} 天中 {count} 天有数", ++ "CI red share": "CI 红占比", + "No snapshot yet.": "还没有快照。", + "Nothing here.": "这里还没有。", + "First load in progress, {seconds} s elapsed": "首次加载中,已 {seconds} 秒", @@ -1346,8 +1356,8 @@ diff --git a/client.js b/client.js } -var eyeMarkup = ([cx, cy, w, h34, tilt]) => ``; -var happyEyeMarkup = ([cx, cy, w, h34, tilt]) => ``; -+var eyeMarkup = ([cx, cy, w, h46, tilt]) => ``; -+var happyEyeMarkup = ([cx, cy, w, h46, tilt]) => ``; ++var eyeMarkup = ([cx, cy, w, h47, tilt]) => ``; ++var happyEyeMarkup = ([cx, cy, w, h47, tilt]) => ``; var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`; var holes = (def, eyes) => `${eyes}`; var MASK_ID = "%MASK%"; @@ -1374,16 +1384,7 @@ diff --git a/client.js b/client.js - for (const listener of [...listeners2]) listener(); - } - }; -+// src/client/cluster-order.js -+function clusterOrder(people2) { -+ const list = Array.isArray(people2) ? people2 : []; -+ const remote = (bot) => Boolean(bot?.remote); -+ if (!list.some(remote)) return list; -+ const locals = list.filter((bot) => !remote(bot)); -+ const remotes = list.filter(remote); -+ if (list.length <= 4) return [...remotes, ...locals]; -+ return [...locals, ...remotes]; - } +-} -var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, 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) { @@ -1504,7 +1505,16 @@ diff --git a/client.js b/client.js - else delete SHAPES[id]; - refresh(); - }; --} ++// src/client/cluster-order.js ++function clusterOrder(people2) { ++ const list = Array.isArray(people2) ? people2 : []; ++ const remote = (bot) => Boolean(bot?.remote); ++ if (!list.some(remote)) return list; ++ const locals = list.filter((bot) => !remote(bot)); ++ const remotes = list.filter(remote); ++ if (list.length <= 4) return [...remotes, ...locals]; ++ return [...locals, ...remotes]; + } -function registerTheme(id, theme) { - if (typeof id !== "string" || !/^[a-z][\w-]{0,31}$/.test(id)) throw new Error("Theme ids are lowercase words"); - for (const mode of ["light", "dark"]) { @@ -1527,6 +1537,9 @@ diff --git a/client.js b/client.js } return spans; } +- +-// src/client/text.js +-var MENTION_ORIGIN = "https://bots.local/"; +function unmatchedMentions(text, bots, { references = [] } = {}) { + const value = String(text ?? ""); + const skip = [ @@ -1548,9 +1561,9 @@ diff --git a/client.js b/client.js + } + return missing; +} - - // src/client/text.js - var MENTION_ORIGIN = "https://bots.local/"; ++ ++// src/client/text.js ++var MENTION_ORIGIN = "https://bots.local/"; function linkMentions(text, bots, selfId) { if (!text || !text.includes("@")) return text; + const ordered = [...bots ?? []].sort((a, b) => Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); @@ -1677,16 +1690,65 @@ diff --git a/client.js b/client.js } return ""; } -@@ -1624,9 +1624,917 @@ function usageOf(list, ids) { +@@ -1624,3156 +1624,6747 @@ function usageOf(list, ids) { const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } +- +-// src/client/motion.js +-var import_react = require("react"); +-var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; +-function springEasing(stiffness, damping) { +- const w0 = Math.sqrt(stiffness); +- const zeta = damping / (2 * w0); +- const at = (t2) => { +- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); +- const wd = w0 * Math.sqrt(1 - zeta * zeta); +- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); +- }; +- let end = 0.05; +- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; +- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); +- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; +function faceRowTitle(title) { + const text = String(title ?? ""); + const named = text.match(/^Face-to-face · (.+) · (\d{6})$/); + if (named) return `${named[2]} · ${named[1]}`; + return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text; -+} + } +-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; @@ -1701,28 +1763,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) { @@ -1735,7 +1853,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); + } @@ -1748,25 +1885,93 @@ 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; + return cache.has(sessionId); -+} + } +-function useLinger(value, ms) { +- const [kept, setKept] = (0, import_react.useState)(value); +- (0, import_react.useEffect)(() => { +- if (value) { +- setKept(value); +- return void 0; +- } +- const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms); +- return () => clearTimeout(timer); +- }, [value]); +- return [value || kept, !value && Boolean(kept)]; +function countCards(state, sessionId, callIds, delta) { + const own = { ...state[sessionId] }; + for (const callId of callIds) { @@ -1775,10 +1980,110 @@ diff --git a/client.js b/client.js + else delete own[callId]; + } + return { ...state, [sessionId]: own }; -+} -+ + } + +-// 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/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"; @@ -1997,7 +2302,42 @@ diff --git a/client.js b/client.js + }) + ); +} -+ + +-.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} +-.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} +-.bt-brand-whale{display:inline-flex;color:var(--bt-accent)} +-.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} +-.bt-round:hover{background:var(--bt-hover)} +-.bt-round:active{transform:scale(.94)} +-.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} +-.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} +-.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} +-.bt-tile:hover{background:var(--bt-hover)} +-.bt-tile[aria-current=page]{background:var(--bt-active)} +-.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} +-.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} +-.bt-row:hover{background:var(--bt-hover)} +-.bt-row[aria-current=page]{background:var(--bt-active)} +-.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} +-.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} +-.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} +-.bt-unread-wait{background:var(--bt-star)} +-.bt-rail{align-items:center;padding:0 0 8px} +-.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} +-.bt-side-foot{display:flex;padding-top:8px} +-.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} +-.bt-connect:hover{background:var(--bt-hover)} +-.bt-connect svg{flex:none;width:15px;height:15px} +-.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0} +-.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center} +-.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}} +// src/client/room-invite-preview.js +var INVITE_PREVIEW_NAME_MAX = 60; +function stripInviteMarkers(text) { @@ -2038,29 +2378,29 @@ diff --git a/client.js b/client.js + const total = Math.max(1, Math.ceil(left / 1e3)); + return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`; +} -+function RoomInvitePreview({ h: h46, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { -+ if (!preview || typeof h46 !== "function") return null; ++function RoomInvitePreview({ h: h47, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { ++ if (!preview || typeof h47 !== "function") return null; + const at = `${kind}`; + const child = (name) => `${at}-${name}`; + const countdown = now === void 0 ? inviteCountdown(preview.expiresAt, preview.clockOffsetMs) : inviteCountdownAt(preview.expiresAt, preview.clockOffsetMs, now); -+ return h46( ++ return h47( + "div", + { className: "fs-preview", "data-flybot": at, role: "status" }, -+ h46("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), -+ preview.lead ? h46("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, -+ preview.people ? h46("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, -+ preview.expiresAt ? h46("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, -+ onJoin || onClear ? h46( ++ h47("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), ++ preview.lead ? h47("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, ++ preview.people ? h47("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, ++ preview.expiresAt ? h47("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, ++ onJoin || onClear ? h47( + "div", + { className: "fs-preview-foot", "data-flybot": child("foot") }, -+ showClear && onClear ? h46("button", { ++ showClear && onClear ? h47("button", { + type: "button", + className: "fs-btn fs-btn-sm", + "data-flybot": child("clear"), + disabled: !!busy, + onClick: onClear + }, clearLabel) : null, -+ onJoin ? h46("button", { ++ onJoin ? h47("button", { + type: "button", + className: "fs-btn fs-btn-sm fs-btn-primary", + "data-flybot": child("join"), @@ -2078,7 +2418,30 @@ diff --git a/client.js b/client.js + const total = Math.max(1, Math.ceil(left / 1e3)); + return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`; +} -+ + +-.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} +-.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} +-.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} +-.bt-pill:active{transform:scale(.965)} +-.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} +-.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} +-.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} +-.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} +-.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)} +-.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms} +-.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)} +-.bt-pill-mem-ico svg{width:16px;height:16px} +-.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both} +-@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}} +-.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0} +-@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}} +-.bt-swap{color:var(--bt-ink-3);font-size:12px} +-.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease} +-.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none} +-.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)} +-.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)} +-@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}} +// src/client/approval-claims.js +var claims = /* @__PURE__ */ new Map(); +var claimListeners = /* @__PURE__ */ new Set(); @@ -2104,13 +2467,105 @@ diff --git a/client.js b/client.js + claimListeners.delete(fn); + }; +} -+ + +-.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} +-.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} +-.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} +-.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} +-.bt-bubble [class*="_markdown"]>*+*{margin-top:10px} +-.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important} +-.bt-bubble p{margin:0} +-.bt-bubble p + p{margin-top:6px} +-.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px} +-.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap} +-.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat} +-.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0} +-.bt-group-msg:first-child{margin-top:0} +-.bt-lead{flex:none;display:flex;margin-bottom:1px} +-button.bt-lead{border:0;background:none;padding:0;cursor:pointer} +-/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */ +-.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px} +-.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px} +-.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} +-.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)} +// src/shared/approval-waiting.js +var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]); +function isApprovalWaiting(status) { + return WAITING_STATUSES.includes(status); +} -+ + +-.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)} +-@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}} +-.bt-q-settled{animation:none} +-.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0} +-.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column} +-.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere} +-.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} +-.bt-q-x svg{width:14px;height:14px} +-.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px} +-.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease} +-.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)} +-.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px} +-.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none} +-button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0} +-.bt-q-raised .bt-q-opt::before{display:none} +-button.bt-q-opt:hover{background:var(--bt-hover)} +-button.bt-q-opt:active{background:var(--bt-active)} +-.bt-q-opt[aria-pressed=true]{background:var(--bt-active)} +-.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)} +-.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease} +-.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px} +-/* With a description the key spans the two text lines less 2px above and below. */ +-.bt-q-opt:has(.bt-q-desc){--bt-key:34px} +-.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px} +-.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0} +-.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere} +-.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere} +-.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-q-settled .bt-q-check{animation:none} +-.bt-q-check svg{width:16px;height:16px} +-.bt-q-settled .bt-key{opacity:.4} +-.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)} +-.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)} +-.bt-q.bt-q-settled{gap:6px;padding:12px 16px} +-.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500} +-.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0} +-.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0} +-.bt-q-settled .bt-q-opt{padding:4px 8px} +-.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box} +-.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0} +-.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease} +-.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)} +-.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere} +-.bt-q-input:not(:placeholder-shown){field-sizing:content} +-.bt-q-input::placeholder{color:var(--bt-ink-3)} +-.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}} +-.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards} +-@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}} +-.bt-q-submit:hover:not(:disabled){opacity:.86} +-.bt-q-submit:disabled{opacity:.4;cursor:default} +-.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-q-btn:hover{background:var(--bt-active)} +-.bt-q-foot{display:flex;justify-content:flex-end;gap:8px} +-.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)} +-.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px} +-.bt-q-raised .bt-q-head:last-child{padding-bottom:16px} +-.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px} +-.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)} +-.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)} +-.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)} +-.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)} +-.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)} +-.bt-q-raised .bt-q-body{padding:2px 6px 0} +-.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)} +-.bt-q-ownrow{cursor:text} +-.bt-q-ownrow .bt-q-input{color:var(--bt-ink)} +-.bt-q-raised .bt-q-foot{padding:0 8px 8px} +-@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} +// src/shared/held-retry.js +var TERMINAL = [ + // A friend's Bot that is gone or no longer shared (plugins/social/src/social.js: botGoneError / notSharedError). @@ -2130,7 +2585,50 @@ diff --git a/client.js b/client.js + const said = `${record.error ?? ""} ${record.code ?? ""}`; + return !TERMINAL.some((reason) => reason.test(said)); +}; -+ + +-.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} +-.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} +-.bt-to-label{color:var(--bt-ink-2)} +-.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} +-.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px} +-.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px} +-.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} +-.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%} +-.bt-option[aria-selected=true]{background:var(--bt-active)} +-.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} +-.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out} +-.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} +-.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} +-.bt-nc-spacer{flex:1} +-.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center} +-.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box} +-.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none} +-.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box} +-.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none} +-.bt-nc-send:disabled{opacity:.3;cursor:default} +-.bt-create{flex:1;display:flex;flex-direction:column;min-width:0} +-.bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box} +-.bt-create-gap{width:28px} +-.bt-create-title{font-size:14px;line-height:20px;font-weight:500} +-.bt-create-body{flex:1;overflow-y:auto;padding:28px 16px 32px;display:flex;justify-content:center} +-.bt-create-card{width:100%;max-width:400px;display:flex;flex-direction:column;gap:18px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .22s ease-out both} +-.bt-create-preview{display:flex;flex-direction:column;align-items:center;gap:10px} +-.bt-create-pop{display:inline-flex;animation:bt-create-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} both} +-@keyframes bt-create-pop{from{transform:scale(.72);opacity:.4}} +-.bt-create-name{font-size:17px;line-height:24px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-create-step{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-bottom:-10px} +-.bt-create-looks{display:flex;flex-direction:column;gap:10px;padding:10px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} +-.bt-create-lookbar{justify-content:center} +-.bt-create-foot{display:flex;justify-content:flex-end;gap:8px;padding-top:4px} +-.bt-create-go{height:36px;padding:0 20px} +-@media (prefers-reduced-motion:reduce){.bt-create-card,.bt-create-pop{animation:none}} +-.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} +-.bt-send:disabled{opacity:.35;cursor:default} +-.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} +// src/client/flybot-slots.js +var import_react2 = require("react"); +var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload); @@ -2388,7 +2886,174 @@ diff --git a/client.js b/client.js +.bt-flybot-approval-files { margin: 0 0 8px; padding-left: 18px; font-size: 12px; } +.bt-file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 0 30px; } +.bt-file-chip { font-size: 12px; padding: 3px 8px; border-radius: 8px; border: 1px solid var(--bt-line, rgba(0,0,0,.15)); background: transparent; cursor: pointer; } -+ + +-.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} +-.bt-dialog .bt-head{height:62px} +-.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box} +-.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px} +-.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both} +-.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none} +-@keyframes bt-sheet-in{from{transform:translateY(24px)}} +-@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}} +-.bt-exchange .bt-dialog{position:relative} +-.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin} +-.bt-exchange .bt-tools{display:none} +-.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both} +-.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none} +-.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards} +-@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}} +-@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}} +-@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}} +-.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer} +-.bt-soft:hover{background:var(--bt-active)} +-.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0} +- +-.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none} +-.bt-panel[data-instant]{transition:none} +-@keyframes bt-pane-in{from{transform:translateX(100%)}} +-[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none} +-.bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)} +-.bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize} +-.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column} +-.bt-drawer-top{display:flex;justify-content:flex-end;gap:8px;padding:12px 17px 6px 12px} +-.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} +-.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} +-.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-drawer-name:hover{background:var(--bt-hover)} +-.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} +-.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} +-.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0} +-.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap} +-.bt-tab:hover{color:var(--bt-ink)} +-.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)} +-.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent} +-.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px} +-.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px} +-.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px} +-.bt-muted{color:var(--bt-ink-2)} +-.bt-swatches{display:flex;flex-wrap:wrap;gap:6px} +-.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)} +-.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)} +-.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical} +-.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center} +-.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)} +-.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums} +-.bt-danger{color:#e02135} +- +-/* Bot settings: fields, the avatar editor popover, and the details-pane sub-view. */ +-.bt-field{display:flex;flex-direction:column;gap:6px} +-.bt-field-label{font-size:13px;line-height:18px;font-weight:500} +-.bt-input{width:100%;box-sizing:border-box;height:36px;border:1px solid var(--bt-line);border-radius:8px;padding:0 10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;outline:none} +-.bt-input:focus{border-color:var(--bt-line-2)} +-.bt-input-area{height:auto;min-height:96px;padding:8px 10px;line-height:18px;resize:vertical} +-.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-3)} +-.bt-soft-sm{height:28px;padding:0 12px;font-size:12px} +-.bt-set-avatar{display:flex;justify-content:center;padding:4px 0} +-.bt-avatar-anchor{position:relative} +-.bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0} +-.bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)} +-.bt-avedit{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:300px;background:var(--bt-main);border:1px solid var(--bt-line);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.18);padding:10px;z-index:70;display:flex;flex-direction:column;gap:10px;color:var(--bt-ink);transform-origin:top center;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-.bt-bs .bt-avedit{transform-origin:32px 0} +-.bt-avedit-top{display:flex;align-items:center;justify-content:space-between} +-.bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px} +-.bt-avedit-tab{position:relative;z-index:1;border:0;background:none;border-radius:999px;padding:3px 10px;font-size:12px;line-height:16px;color:var(--bt-ink-2);cursor:pointer} +-.bt-avedit-tab[aria-selected=true]{background:var(--bt-main);color:var(--bt-ink)} +-.bt-avedit-tabs>.bt-thumb{top:2px;bottom:2px;border-radius:999px;background:var(--bt-main);box-shadow:0 1px 2px rgba(0,0,0,.06)} +-.bt-avedit-tabs[data-glide] .bt-avedit-tab[aria-selected=true]{background:transparent} +-.bt-avedit-reset{color:var(--bt-ink-3)} +-.bt-avedit-reset:hover{color:var(--bt-ink)} +-.bt-shapes{display:flex;flex-direction:column;gap:2px} +-.bt-shape-row{display:grid;grid-template-columns:repeat(6,1fr);gap:2px} +-.bt-shape{position:relative;aspect-ratio:1;border:0;border-radius:8px;background:none;color:var(--bt-ink);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0} +-.bt-shape:hover{background:var(--bt-hover)} +-.bt-shape .bt-ring{position:absolute;left:50%;top:50%;width:36px;height:36px;translate:-50% -50%;overflow:visible;opacity:0;scale:.82;pointer-events:none;transition:opacity .14s ease,${spring(SPRING_POP, "scale")}} +-.bt-shape[aria-pressed=true] .bt-ring{opacity:1;scale:1} +-.bt-colors{display:flex;justify-content:space-between} +-.bt-color{width:22px;height:22px;border-radius:50%;border:2px solid transparent;padding:2px;background:none;cursor:pointer;box-sizing:border-box;transition:border-color .14s ease,${spring(SPRING_TAP, "scale")}} +-.bt-color[aria-pressed=true] span{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-color span{display:block;width:100%;height:100%;border-radius:50%} +-.bt-color[aria-pressed=true]{border-color:var(--bt-ink)} +-.bt-dropzone{border:1.5px dashed var(--bt-line-2);border-radius:10px;padding:18px 12px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--bt-ink-2);font-size:12px;line-height:16px;text-align:center} +-.bt-dropzone[data-over]{border-color:var(--bt-accent);color:var(--bt-ink)} +-.bt-dropzone img{width:64px;height:64px;border-radius:50%;object-fit:cover} +-.bt-set-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center} +-.bt-about{white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden} +-.bt-about[data-open]{display:block;-webkit-line-clamp:unset} +-.bt-more{border:0;background:none;color:var(--bt-ink-3);font-size:12px;line-height:16px;cursor:pointer;padding:2px 0} +-.bt-more:hover{color:var(--bt-ink)} +-.bt-subhead{display:flex;align-items:center;gap:4px;padding:10px 12px 4px} +-.bt-subhead-title{flex:1;font-size:15px;line-height:24px;font-weight:600;padding-left:4px} +-.bt-icon-btn{width:28px;height:28px;border-radius:50%;border:0;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0} +-.bt-icon-btn:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-set-body{gap:20px} +-.bt-drawer-model{display:flex;flex-direction:column} +-.bt-drawer-model .bt-note{margin-top:6px} +-.bt-drawer-permission{display:flex;flex-direction:column} +-.bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135} +- +-/* Settings dialog: a 198px page list beside the page. */ +-.bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:60;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-scrim{position:absolute;inset:0;background:rgba(0,0,0,.32);animation:bt-fade .22s ease-out} +-body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)} +-.bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))} +-:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none} +-:is(.bt-settings-layer,.bt-mem-layer)[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} +-:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]>[role=dialog]{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards} +-@keyframes bt-dialog-rise{from{transform:translateY(12px) scale(.96)}} +-@keyframes bt-dialog-sink{to{opacity:0;transform:translateY(6px) scale(.975);filter:blur(3px)}} +-@media (prefers-reduced-motion:reduce){.bt-settings{transition:none}.bt-settings,.bt-scrim,.bt-settings-page>*{animation:bt-fade .12s ease-out!important}} +-.bt-settings-nav{position:relative;width:198px;flex:none;padding:16px 8px;border-right:.5px solid var(--bt-line);display:flex;flex-direction:column;gap:2px;background:var(--bt-sidebar);box-sizing:border-box} +-.bt-settings-item{position:relative;z-index:1;display:flex;align-items:center;gap:8px;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left;transition:background-color .12s ease,color .12s ease} +-.bt-settings-item:hover{color:var(--bt-ink);background:var(--bt-hover)} +-.bt-settings-item[aria-current=page]{background:var(--bt-active);color:var(--bt-ink)} +-.bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} +-.bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} +-.bt-settings-shell{margin-top:auto} +-/* Usage while another page is open: out of the flow at Usage's own width, so it keeps +- its layout, and not rendered. */ +-.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} +-/* The page column takes the dialog's final size at once, so while the dialog resizes +- only its edge moves and the page does not lay out again every frame. */ +-.bt-settings-main{flex:none;display:flex;flex-direction:column;min-width:0;width:calc(var(--bt-set-w) - 198px);height:var(--bt-set-h)} +-.bt-settings-head{display:flex;align-items:center;gap:6px;padding:16px 56px 4px 20px} +-.bt-settings-head h2{margin:0;font-size:15px;line-height:24px;font-weight:600;animation:bt-veil-in .2s ease-out} +-.bt-settings-head>.bt-icon-btn{animation:bt-back-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-@keyframes bt-back-in{from{translate:6px 0;scale:.8}} +-.bt-settings-scroll{flex:1;overflow-y:auto;padding:12px 20px 24px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin} +-.bt-settings-page>:not(.bt-us){animation:bt-page-up ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .22s ease-out backwards} +-.bt-settings-page[data-move=in]>:not(.bt-us){animation-name:bt-page-in,bt-veil-in} +-.bt-settings-page[data-move=out]>:not(.bt-us){animation-name:bt-page-out,bt-veil-in} +-.bt-settings-page>:nth-child(2){animation-delay:30ms} +-.bt-settings-page>:nth-child(3){animation-delay:60ms} +-.bt-settings-page>:nth-child(4){animation-delay:90ms} +-.bt-settings-page>:nth-child(n+5){animation-delay:120ms} +-@keyframes bt-page-up{from{transform:translateY(10px)}} +-@keyframes bt-page-in{from{transform:translateX(18px)}} +-@keyframes bt-page-out{from{transform:translateX(-18px)}} +-.bt-settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0} +-.bt-settings-close:hover{background:var(--bt-active);color:var(--bt-ink)} +-.bt-settings-form{display:flex;flex-direction:column;gap:16px} +-.bt-set-section h3{font-size:13px;line-height:18px;font-weight:600;margin:0 0 8px;padding:0 4px} +-.bt-set-card{border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card);display:flex;flex-direction:column} +-.bt-set-card>*+*{border-top:.5px solid var(--bt-line)} +-.bt-set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;text-align:left;width:100%;box-sizing:border-box;border-radius:0} +-button.bt-set-row{cursor:pointer} +-button.bt-set-row:hover{background:var(--bt-hover)} +-.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} +-.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} +-.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} +-.bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0} +-.bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))} +-.bt-set-control{flex:none;display:flex;align-items:center;gap:6px} +-.bt-set-value{font-size:13px;color:var(--bt-ink-2);text-align:right} +-.bt-set-bot{flex:1;display:flex;align-items:center;gap:8px;min-width:0} +-.bt-set-paste{flex-direction:column;align-items:stretch;gap:8px} +-.bt-chevron{color:var(--bt-ink-3);display:inline-flex;flex:none} +.bt-flybot-section { margin-top: 8px; border-top: 1px solid var(--bt-line, rgba(0,0,0,.08)); padding-top: 6px; } +/* A section whose component renders null leaves the wrapper empty, and an empty wrapper must not draw + the separator: #818's "no border" held for the component but not for the box around it. Keyed on @@ -2417,7 +3082,21 @@ diff --git a/client.js b/client.js +.bt-flybot-pointer-note { opacity: .65; white-space: nowrap; } +.bt-flybot-pointer button { border: 0; border-radius: 999px; padding: 3px 10px; font-size: 12px; cursor: pointer; background: var(--bt-accent, #2563eb); color: #fff; } +`; -+ + +-/* Settings → Connectors. */ +-.bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px} +-.bt-connector-head{display:flex;align-items:center;gap:12px;min-width:0} +-.bt-connector-mark{flex:none;width:36px;height:36px;border-radius:10px;background:var(--bt-card);border:.5px solid var(--bt-line);display:flex;align-items:center;justify-content:center;color:var(--bt-ink)} +-.bt-connector-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} +-.bt-connector-name{font-size:14px;line-height:20px;font-weight:500} +-.bt-connector-status{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-connector[data-state=ready] .bt-connector-status{color:var(--bt-ink-2)} +-.bt-connector[data-state=error] .bt-connector-status{color:#e02135;white-space:normal} +-.bt-connector-actions{flex:none;display:flex;gap:6px} +-.bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-connector-form-row{display:flex;align-items:center;gap:6px} +-.bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)} +-.bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135} +// src/shared/turn-failures.js +var FAILURE_LINES_SHOWN = 3; +var failureLines = (snapshot, place, t2, limit = FAILURE_LINES_SHOWN) => { @@ -2428,7 +3107,15 @@ diff --git a/client.js b/client.js + ...Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {} + })); +}; -+ + +-/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the +- shell offers them and CSS-module name suffixes elsewhere. */ +-[class*="_logoRow"],[class*="_newSession"],nav[class*="_panelList"]{display:none!important} +-[class*="_footArea"]{display:flex!important;flex-direction:row-reverse;align-items:center;gap:9px;padding:8px 6px 6px!important} +-[class*="_footArea"]>[class*="_footerActions"]{flex:1 1 auto;min-width:0} +-[class*="_footArea"]>[class*="_settingsArea"]{flex:none;width:auto!important} +-[class*="_footArea"] button[aria-label="Settings"]{width:auto!important;padding:0!important;background:none!important;border-radius:50%!important;height:auto!important} +-[class*="_widthHandle"]{display:none!important} +// src/client/turn-failures.js +var import_react3 = require("react"); +function TurnFailures({ sessionId, work }) { @@ -2458,7 +3145,66 @@ diff --git a/client.js b/client.js +.bt-turn-failure{display:flex;gap:6px;align-items:baseline;padding:6px 10px;border-radius:8px;font-size:12px;line-height:1.45;background:color-mix(in srgb,var(--bt-ink) 6%,transparent);color:color-mix(in srgb,var(--bt-ink) 72%,transparent)} +.bt-turn-failure-streak{opacity:.72} +`; -+ + +-.bt-voice-pane{flex-direction:column;align-items:center;justify-content:center;animation:bt-fade .2s ease-out} +-.bt-voice-stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:24px;max-width:560px;text-align:center} +-.bt-voice-orb{position:relative;width:200px;height:200px;display:grid;place-items:center;margin-bottom:8px} +-.bt-voice-ring{position:absolute;inset:34px;border-radius:50%;background:var(--bt-hover);transform:scale(calc(1 + var(--bt-level,0) * .55));transition:transform .08s linear} +-.bt-voice-ring-2{inset:14px;background:none;box-shadow:inset 0 0 0 1px var(--bt-line-2);transform:scale(calc(.94 + var(--bt-level,0) * .3));opacity:.8} +-.bt-voice-stage[data-phase=thinking] .bt-voice-ring{animation:bt-voice-breathe 1.6s ease-in-out infinite} +-.bt-voice-stage[data-phase=speaking] .bt-voice-ring{animation:bt-voice-speak .9s ease-in-out infinite} +-@keyframes bt-voice-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}} +-@keyframes bt-voice-speak{0%,100%{transform:scale(1.04)}30%{transform:scale(1.2)}60%{transform:scale(1.1)}} +-.bt-voice-orb>.bt-mark{position:relative} +-.bt-voice-name{font-size:16px;line-height:22px;font-weight:500} +-.bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px} +-.bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} +-.bt-voice-note{font-size:12px;color:#e02135} +-.bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease} +-.bt-voice-setup:hover{background:var(--bt-active)} +-.bt-voice-bar{display:flex;gap:16px;padding:0 0 32px} +-.bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease} +-.bt-voice-btn:hover{background:var(--bt-active)} +-.bt-voice-btn:active{transform:scale(.94)} +-.bt-voice-btn[aria-pressed=true]{background:var(--bt-user);color:var(--bt-user-ink)} +-.bt-voice-slash{position:absolute;width:24px;height:2px;border-radius:1px;background:currentColor;transform:rotate(-45deg)} +-.bt-voice-end{background:#ff3e51;color:#fff} +-.bt-voice-end:hover{background:#e02135} +-@keyframes bt-fade{from{opacity:0}to{opacity:1}} +-@keyframes bt-blur-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}} +-@media (prefers-reduced-motion:reduce){.bt-voice-pane,.bt-voice-caption,.bt-voice-ring{animation:none!important}} +-.bt-msg{display:flex;flex-direction:column;align-items:flex-start;max-width:min(80%,560px,calc(100% - 82px))} +-/* The bubble's row: reactions hang below it, so the hover toolbar (and a group +- sender's avatar) line up with the bubble alone. */ +-.bt-msg-line{position:relative;display:flex;align-items:flex-end;gap:8px;max-width:100%} +-.bt-msg-line>.bt-bubble{max-width:none;min-width:0} +-/* The same bubble width as beside an avatar column: 80% of (100% - 30px), plus the 30px. */ +-.bt-astack[data-lead]>.bt-msg{max-width:min(calc(80% + 6px),590px,calc(100% - 52px))} +-.bt-tools{position:absolute;top:50%;left:100%;margin-left:6px;transform:translateY(-50%);display:flex;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s} +-.bt-tools::after{content:"";position:absolute;top:-8px;bottom:-8px;right:100%;width:128px;z-index:-1} +-.bt-msg:hover>.bt-msg-line>.bt-tools,.bt-msg:focus-within>.bt-msg-line>.bt-tools,.bt-tools[data-open]{opacity:1;visibility:visible} +-.bt-tool{width:24px;height:24px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease} +-.bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-tool:active{background:var(--bt-active)} +-.bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-.bt-react-strip{position:fixed;z-index:60;pointer-events:auto;display:flex;gap:2px;padding:4px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.08),0 4px 6px -4px rgba(0,0,0,.06);transform:translate(-50%,-100%);transform-origin:bottom center;animation:bt-pop-lift ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} +-.bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-react-strip button:nth-child(2){animation-delay:20ms} +-.bt-react-strip button:nth-child(3){animation-delay:40ms} +-.bt-react-strip button:nth-child(4){animation-delay:60ms} +-.bt-react-strip button:nth-child(n+5){animation-delay:80ms} +-.bt-react-strip button:hover{background:var(--bt-hover);transform:scale(1.15)} +-.bt-react-strip button:active{transform:scale(.92)} +-.bt-reacts{display:flex;gap:4px;margin-top:4px} +-.bt-react{height:22px;min-width:30px;padding:0 6px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);font-size:13px;line-height:20px;cursor:pointer;animation:bt-react-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing};transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} +-.bt-react:hover{background:var(--bt-hover)} +-.bt-react:active{scale:.92} +-@keyframes bt-react-pop{from{transform:scale(.6);opacity:0}} +-.bt-bubble a[href^="${MENTION_ORIGIN}"] svg{display:none} +-.bt-you{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,var(--bt-sidebar));color:var(--bt-ink-2);box-shadow:inset 0 0 0 1px var(--bt-line);transition:background-color .12s ease} +-button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} +-.bt-you-wrap{display:inline-flex;padding:0} +-.bt-connect-foot{width:100%;height:36px} +// src/client/themes.js +var THEME_KEYS = ["bg", "sidebar", "bubble", "card", "text", "user", "userText", "accent"]; +var THEMES = { @@ -2593,112 +3339,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 +1659,12 @@ function liveSprings(apply2) { - const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); - last = now; - const steps = Math.ceil(dt * 240); -- const h34 = dt / steps; -+ const h46 = 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; -+ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h46; -+ value.x += value.v * h46; - } - if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { - value.x = value.to; -@@ -1699,10 +1699,10 @@ function liveSprings(apply2) { - } - var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; - function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { -- const ownBox = (0, import_react.useRef)(null); ++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 h47 = 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) * h47; ++ value.x += value.v * h47; ++ } ++ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { ++ value.x = value.to; ++ value.v = 0; ++ } else moving = true; ++ } ++ apply2(read()); ++ frame = moving ? requestAnimationFrame(step) : 0; ++ }; ++ return { ++ target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, ++ set(targets, curves, jump) { ++ for (const [key, to] of Object.entries(targets)) { ++ const value = values.get(key); ++ const { stiffness: k, damping: c } = curves[key]; ++ if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); ++ else Object.assign(value, { to, k, c }); ++ } ++ if (jump) { ++ cancelAnimationFrame(frame); ++ frame = 0; ++ apply2(read()); ++ return; ++ } ++ if (!frame) { ++ last = performance.now(); ++ frame = requestAnimationFrame(step); ++ } ++ }, ++ stop: () => cancelAnimationFrame(frame) ++ }; ++} ++var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; ++function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { + const ownBox = (0, import_react4.useRef)(null); - const box = given ?? ownBox; -- const thumb = (0, import_react.useRef)(null); -- const own = (0, import_react.useRef)(null); ++ const box = given ?? ownBox; + const thumb = (0, import_react4.useRef)(null); + const own = (0, import_react4.useRef)(null); - if (own.current === null) { - own.current = { - key: void 0, -@@ -1736,7 +1736,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); - container.dataset.glide = ""; - }; -- (0, import_react.useLayoutEffect)(() => { ++ if (own.current === null) { ++ own.current = { ++ key: void 0, ++ node: null, ++ observer: null, ++ springs: liveSprings(({ start, end }) => { ++ const knob = thumb.current; ++ if (!knob) return; ++ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; ++ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; ++ }) ++ }; ++ } ++ const place = (jump) => { ++ const container = box.current; ++ const knob = thumb.current; ++ if (!container || !knob) return; ++ const chosen = container.querySelector(selector); ++ if (!chosen) { ++ knob.dataset.hidden = ""; ++ return; ++ } ++ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; ++ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); ++ const { springs } = own.current; ++ const before = springs.target(); ++ const hidden = knob.dataset.hidden !== void 0; ++ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; ++ delete knob.dataset.hidden; ++ const forward = before === null || start >= before.start; ++ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); ++ container.dataset.glide = ""; ++ }; + (0, import_react4.useLayoutEffect)(() => { - const state = own.current; - const node = box.current; - if (node === state.node && key === state.key) return; -@@ -1750,7 +1750,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - } - place(fresh); - }); -- (0, import_react.useEffect)(() => () => { ++ const state = own.current; ++ const node = box.current; ++ if (node === state.node && key === state.key) return; ++ const fresh = node !== state.node; ++ state.key = key; ++ if (fresh) { ++ state.observer?.disconnect(); ++ state.node = node; ++ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; ++ state.observer?.observe(node); ++ } ++ place(fresh); ++ }); + (0, import_react4.useEffect)(() => () => { - own.current.observer?.disconnect(); - own.current.springs.stop(); - }, []); -@@ -1758,16 +1758,16 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai - } - function Segmented({ label, value, className, children }) { - const glide = useGlide(value); -- return (0, import_react.createElement)( ++ own.current.observer?.disconnect(); ++ own.current.springs.stop(); ++ }, []); ++ return { box, thumb }; ++} ++function Segmented({ label, value, className, children }) { ++ const glide = useGlide(value); + return (0, import_react4.createElement)( - "div", - { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, -- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ "div", ++ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, + (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - children - ); - } - function useFlip(list, key, selector, id = (node) => node.dataset.flip) { -- const tops = (0, import_react.useRef)(null); -- (0, import_react.useLayoutEffect)(() => { ++ children ++ ); ++} ++function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + const tops = (0, import_react4.useRef)(null); + (0, import_react4.useLayoutEffect)(() => { - const container = list.current; - if (!container) return; - const next = /* @__PURE__ */ new Map(); -@@ -1788,8 +1788,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) { - }, [key]); - } - function useMorph(box, key) { -- const last = (0, import_react.useRef)({ key, height: null }); -- (0, import_react.useLayoutEffect)(() => { ++ const container = list.current; ++ if (!container) return; ++ const next = /* @__PURE__ */ new Map(); ++ const moving = tops.current !== null && !reducedMotion(); ++ for (const node of container.querySelectorAll(selector)) { ++ const name = id(node); ++ const top = node.offsetTop; ++ next.set(name, top); ++ if (!moving) continue; ++ const before = tops.current.get(name); ++ if (before === void 0) { ++ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); ++ } else if (Math.abs(before - top) > 0.5) { ++ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); ++ } ++ } ++ tops.current = next; ++ }, [key]); ++} ++function useMorph(box, key) { + const last = (0, import_react4.useRef)({ key, height: null }); + (0, import_react4.useLayoutEffect)(() => { - const node = box.current; - const state = last.current; - if (!node) { -@@ -1817,8 +1817,8 @@ function useMorph(box, key) { - }); - } - function useLinger(value, ms) { -- const [kept, setKept] = (0, import_react.useState)(value); -- (0, import_react.useEffect)(() => { ++ const node = box.current; ++ const state = last.current; ++ if (!node) { ++ last.current = { key, height: null }; ++ return; ++ } ++ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); ++ if (key === state.key) { ++ if (running.length === 0) state.height = node.offsetHeight; ++ return; ++ } ++ const from = running.length ? node.offsetHeight : state.height; ++ for (const animation2 of running) animation2.cancel(); ++ const to = node.offsetHeight; ++ last.current = { key, height: to }; ++ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; ++ node.dataset.morph = ""; ++ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); ++ animation.onfinish = () => { ++ delete node.dataset.morph; ++ }; ++ animation.oncancel = () => { ++ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; ++ }; ++ }); ++} ++function useLinger(value, ms) { + const [kept, setKept] = (0, import_react4.useState)(value); + (0, import_react4.useEffect)(() => { - if (value) { - setKept(value); - return void 0; -@@ -1838,6 +1838,7 @@ ${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt- - .bt-mark{display:inline-block;flex:none;position:relative;line-height:0} - .bt-mark svg{width:100%;height:100%;overflow:visible} - .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} ++ if (value) { ++ setKept(value); ++ return void 0; ++ } ++ const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms); ++ return () => clearTimeout(timer); ++ }, [value]); ++ return [value || kept, !value && Boolean(kept)]; ++} ++ ++// src/client/styles.js ++var SHELL_CSS = ` ++body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)} ++body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)} ++${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")} ++${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")} ++.bt-mark{display:inline-block;flex:none;position:relative;line-height:0} ++.bt-mark svg{width:100%;height:100%;overflow:visible} ++.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} +.bt-mark-emoji{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;user-select:none} - .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} - .bt-mark .bt-b{transform-origin:50% 100%} - .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} -@@ -1856,6 +1856,14 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo - @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} - body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} - body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} ++.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} ++.bt-mark .bt-b{transform-origin:50% 100%} ++.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} ++/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a ++ mark only for its state; normal, the default, adds petting, pokes and eyes that follow ++ the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */ ++body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)} ++body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite} ++body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop} ++body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway} ++body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch} ++body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite} ++@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}} ++@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}} ++@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}} ++@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} ++body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} ++body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} +.bt-tree-item{position:relative;display:flex;align-items:center;min-width:0} +.bt-tree-item>.bt-row{flex:1 1 auto;min-width:0} +.bt-tree-toggle{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);font-size:16px;line-height:1;cursor:pointer} @@ -2707,14 +3577,83 @@ diff --git a/client.js b/client.js +.bt-tree-chev{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:16px;line-height:1;transition:transform .12s ease} +.bt-tree-toggle[aria-expanded=false] .bt-tree-chev{transform:rotate(-90deg)} +.bt-tree-leaf{cursor:default} - @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} - /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its - size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -@@ -1933,15 +1933,23 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} - .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} - .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} - .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} --.bt-brand-whale{display:inline-flex;color:var(--bt-accent)} ++@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} ++/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its ++ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes ++ closed in a happy arc, and springs back when the pointer leaves. */ ++:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%} ++.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out} ++.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in} ++.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out} ++body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} ++body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none} ++body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s} ++@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}} ++.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out} ++.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out} ++body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none} ++body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none} ++.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite} ++.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite} ++.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite} ++.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite} ++.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite} ++.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%} ++.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite} ++.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite} ++.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite} ++.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%} ++.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite} ++/* A question for you hops twice when it arrives and the badge keeps it in view after; ++ a lively mark keeps hopping. */ ++.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} ++.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite} ++.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} ++body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite} ++.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1} ++.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%} ++@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}} ++.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important} ++.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important} ++.bt-mark[data-poke]{cursor:pointer} ++body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important} ++body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} ++@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}} ++@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}} ++@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}} ++@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}} ++@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}} ++@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}} ++@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}} ++@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}} ++@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}} ++@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}} ++@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}} ++@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}} ++@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}} ++/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */ ++@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}} ++@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}} ++@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}} ++.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)} ++.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))} ++.bt-badge-admin svg{width:7px;height:7px} ++.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none} ++.bt-badge-main svg{width:100%;height:100%;overflow:visible} ++.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke} ++.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px} ++.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0} ++.bt-cluster>*{position:absolute} ++.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums} ++.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)} ++.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0} ++.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)} ++.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important} ++ ++.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} ++.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} +.bt-brand-mark{display:inline-flex;line-height:0} +.bt-flybot-mark{display:block;flex:none} +.bt-flybot-mark .bt-flybot-wing{fill:#8FB2FF} @@ -2723,152 +3662,700 @@ diff --git a/client.js b/client.js +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-wing{fill:#A9C2FF} +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-body{fill:#6E87FF} +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-eye{fill:#0F1530} - .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} - .bt-round:hover{background:var(--bt-hover)} - .bt-round:active{transform:scale(.94)} - .bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} - .bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} --.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} ++.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} ++.bt-round:hover{background:var(--bt-hover)} ++.bt-round:active{transform:scale(.94)} ++.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} ++.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} +.bt-tile{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} - .bt-tile:hover{background:var(--bt-hover)} - .bt-tile[aria-current=page]{background:var(--bt-active)} ++.bt-tile:hover{background:var(--bt-hover)} ++.bt-tile[aria-current=page]{background:var(--bt-active)} +.bt-tile-unread{position:absolute;top:8px;right:12px} - .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} - .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} -@@ -1950,27 +1950,32 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} - .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} - .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} - .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} ++.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} ++.bt-row:hover{background:var(--bt-hover)} ++.bt-row[aria-current=page]{background:var(--bt-active)} ++.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} ++.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} ++.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-row-title.bt-row-title-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;overflow:hidden} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} - .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} - .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} - .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} - .bt-unread-wait{background:var(--bt-star)} ++.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} ++.bt-unread-wait{background:var(--bt-star)} +.bt-shared-from,.bt-shared-wait{display:flex;flex-direction:column;gap:6px;padding:8px 0} +.bt-shared-wait .bt-set-row{min-width:0} +.bt-shared-wait .bt-set-copy{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block} - .bt-rail{align-items:center;padding:0 0 8px} - .bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} - .bt-side-foot{display:flex;padding-top:8px} - .bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} - .bt-connect:hover{background:var(--bt-hover)} - .bt-connect svg{flex:none;width:15px;height:15px} --.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0} --.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center} --.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-rail{align-items:center;padding:0 0 8px} ++.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} ++.bt-side-foot{display:flex;padding-top:8px} ++.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} ++.bt-connect:hover{background:var(--bt-hover)} ++.bt-connect svg{flex:none;width:15px;height:15px} +.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}ms ${SPRING_SHAPE.easing}} - @keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} --@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}} ++@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +@media (prefers-reduced-motion:reduce){} - - .bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} - .bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} - .bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} - .bt-pill:active{transform:scale(.965)} - .bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} ++ ++.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} ++.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} ++.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} ++.bt-pill:active{transform:scale(.965)} ++.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} +.bt-pill:has(.bt-pill-name-face){height:auto;min-height:40px;align-items:flex-start} +.bt-pill-name.bt-pill-name-face{min-width:0;white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden} - .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} - .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} - .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} -@@ -1992,6 +1992,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} - - .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} - .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} ++.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} ++.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} ++.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} ++.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)} ++.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms} ++.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)} ++.bt-pill-mem-ico svg{width:16px;height:16px} ++.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both} ++@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}} ++.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0} ++@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}} ++.bt-swap{color:var(--bt-ink-3);font-size:12px} ++.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease} ++.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none} ++.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)} ++.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)} ++@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}} ++ ++.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} ++.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} +.bt-event.bt-not-sent{gap:6px;color:var(--bt-ink-3)} - .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} - .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} - .bt-bubble [class*="_markdown"]>*+*{margin-top:10px} -@@ -2085,6 +2085,7 @@ button.bt-q-opt:active{background:var(--bt-active)} - @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} - - .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} ++.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} ++.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} ++.bt-bubble [class*="_markdown"]>*+*{margin-top:10px} ++.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important} ++.bt-bubble p{margin:0} ++.bt-bubble p + p{margin-top:6px} ++.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px} ++.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap} ++.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat} ++.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0} ++.bt-group-msg:first-child{margin-top:0} ++.bt-lead{flex:none;display:flex;margin-bottom:1px} ++button.bt-lead{border:0;background:none;padding:0;cursor:pointer} ++/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */ ++.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px} ++.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px} ++.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} ++.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)} ++ ++.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} ++body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)} ++@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}} ++.bt-q-settled{animation:none} ++.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0} ++.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column} ++.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere} ++.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} ++.bt-q-x svg{width:14px;height:14px} ++.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} ++.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px} ++.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease} ++.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)} ++.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px} ++.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none} ++button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0} ++.bt-q-raised .bt-q-opt::before{display:none} ++button.bt-q-opt:hover{background:var(--bt-hover)} ++button.bt-q-opt:active{background:var(--bt-active)} ++.bt-q-opt[aria-pressed=true]{background:var(--bt-active)} ++.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)} ++.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease} ++.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px} ++/* With a description the key spans the two text lines less 2px above and below. */ ++.bt-q-opt:has(.bt-q-desc){--bt-key:34px} ++.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px} ++.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0} ++.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere} ++.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere} ++.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} ++.bt-q-settled .bt-q-check{animation:none} ++.bt-q-check svg{width:16px;height:16px} ++.bt-q-settled .bt-key{opacity:.4} ++.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)} ++.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)} ++.bt-q.bt-q-settled{gap:6px;padding:12px 16px} ++.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500} ++.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0} ++.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0} ++.bt-q-settled .bt-q-opt{padding:4px 8px} ++.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box} ++.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0} ++.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease} ++.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)} ++.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere} ++.bt-q-input:not(:placeholder-shown){field-sizing:content} ++.bt-q-input::placeholder{color:var(--bt-ink-3)} ++.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}} ++.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards} ++@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}} ++.bt-q-submit:hover:not(:disabled){opacity:.86} ++.bt-q-submit:disabled{opacity:.4;cursor:default} ++.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-q-btn:hover{background:var(--bt-active)} ++.bt-q-foot{display:flex;justify-content:flex-end;gap:8px} ++.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)} ++.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px} ++.bt-q-raised .bt-q-head:last-child{padding-bottom:16px} ++.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px} ++.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)} ++.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)} ++.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)} ++.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)} ++.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)} ++.bt-q-raised .bt-q-body{padding:2px 6px 0} ++.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)} ++.bt-q-ownrow{cursor:text} ++.bt-q-ownrow .bt-q-input{color:var(--bt-ink)} ++.bt-q-raised .bt-q-foot{padding:0 8px 8px} ++@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} ++ ++.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} +.bt-pane[data-menu]{background:transparent}.bt-pane[data-menu] .bt-newchat{flex:none;align-self:flex-start;width:min(280px,calc(100% - 16px));max-height:calc(100% - 16px);margin:8px;overflow:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.06)}.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:0;box-shadow:none;border:0;max-width:none} - .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} - .bt-to-label{color:var(--bt-ink-2)} - .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} -@@ -2098,7 +2098,9 @@ button.bt-q-opt:active{background:var(--bt-active)} - .bt-option[aria-selected=true]{background:var(--bt-active)} - .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} - .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out} ++.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} ++.bt-to-label{color:var(--bt-ink-2)} ++.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} ++.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px} ++.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px} ++.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} ++.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} ++.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%} ++.bt-option[aria-selected=true]{background:var(--bt-active)} ++.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} ++.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:0 1 auto;min-width:0;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;animation:bt-veil-in .16s ease-out} +.bt-option-copy{flex:1;min-width:0;display:flex;flex-direction:column} +.bt-option-sub{font-size:11px;line-height:15px;color:var(--bt-ink-2);white-space:normal;overflow-wrap:anywhere} - .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} - .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} - .bt-nc-spacer{flex:1} -@@ -2127,6 +2127,10 @@ button.bt-q-opt:active{background:var(--bt-active)} - .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} - .bt-send:disabled{opacity:.35;cursor:default} - .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} ++.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} ++.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} ++.bt-nc-spacer{flex:1} ++.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center} ++.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box} ++.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none} ++.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box} ++.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none} ++.bt-nc-send:disabled{opacity:.3;cursor:default} ++.bt-create{flex:1;display:flex;flex-direction:column;min-width:0} ++.bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box} ++.bt-create-gap{width:28px} ++.bt-create-title{font-size:14px;line-height:20px;font-weight:500} ++.bt-create-body{flex:1;overflow-y:auto;padding:28px 16px 32px;display:flex;justify-content:center} ++.bt-create-card{width:100%;max-width:400px;display:flex;flex-direction:column;gap:18px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .22s ease-out both} ++.bt-create-preview{display:flex;flex-direction:column;align-items:center;gap:10px} ++.bt-create-pop{display:inline-flex;animation:bt-create-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} both} ++@keyframes bt-create-pop{from{transform:scale(.72);opacity:.4}} ++.bt-create-name{font-size:17px;line-height:24px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-create-step{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-bottom:-10px} ++.bt-create-looks{display:flex;flex-direction:column;gap:10px;padding:10px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} ++.bt-create-lookbar{justify-content:center} ++.bt-create-foot{display:flex;justify-content:flex-end;gap:8px;padding-top:4px} ++.bt-create-go{height:36px;padding:0 20px} ++@media (prefers-reduced-motion:reduce){.bt-create-card,.bt-create-pop{animation:none}} ++.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} ++.bt-send:disabled{opacity:.35;cursor:default} ++.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} +.bt-fix-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px} +.bt-fix-btn{font:inherit;font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer} +.bt-fix .bt-note{margin-top:6px} +.bt-fix-link{margin-top:6px;color:inherit;user-select:all} - - .bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} - .bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} -@@ -2163,6 +2163,8 @@ button.bt-q-opt:active{background:var(--bt-active)} - .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} - .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} - .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++ ++.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} ++.bt-dialog .bt-head{height:62px} ++.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box} ++.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px} ++.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both} ++.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none} ++@keyframes bt-sheet-in{from{transform:translateY(24px)}} ++@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}} ++.bt-exchange .bt-dialog{position:relative} ++.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin} ++.bt-exchange .bt-tools{display:none} ++.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both} ++.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none} ++.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards} ++@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}} ++@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}} ++@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}} ++.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer} ++.bt-soft:hover{background:var(--bt-active)} ++.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0} ++ ++.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none} ++.bt-panel[data-instant]{transition:none} ++@keyframes bt-pane-in{from{transform:translateX(100%)}} ++[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++.bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none} ++.bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)} ++.bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize} ++.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column} ++.bt-drawer-top{display:flex;justify-content:flex-end;gap:8px;padding:12px 17px 6px 12px} ++.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} ++.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} ++.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.bt-drawer-name-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;text-overflow:ellipsis;line-clamp:3;cursor:default} +.bt-drawer-name-face:hover{background:none} - .bt-drawer-name:hover{background:var(--bt-hover)} - .bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} - .bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} -@@ -2255,6 +2255,7 @@ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)} - .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} - .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} - .bt-settings-shell{margin-top:auto} ++.bt-drawer-name:hover{background:var(--bt-hover)} ++.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} ++.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} ++.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0} ++.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap} ++.bt-tab:hover{color:var(--bt-ink)} ++.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)} ++.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent} ++.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px} ++.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px} ++.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px} ++.bt-muted{color:var(--bt-ink-2)} ++.bt-swatches{display:flex;flex-wrap:wrap;gap:6px} ++.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)} ++.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)} ++.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical} ++.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center} ++.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)} ++.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums} ++.bt-danger{color:#e02135} ++ ++/* Bot settings: fields, the avatar editor popover, and the details-pane sub-view. */ ++.bt-field{display:flex;flex-direction:column;gap:6px} ++.bt-field-label{font-size:13px;line-height:18px;font-weight:500} ++.bt-input{width:100%;box-sizing:border-box;height:36px;border:1px solid var(--bt-line);border-radius:8px;padding:0 10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;outline:none} ++.bt-input:focus{border-color:var(--bt-line-2)} ++.bt-input-area{height:auto;min-height:96px;padding:8px 10px;line-height:18px;resize:vertical} ++.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-3)} ++.bt-soft-sm{height:28px;padding:0 12px;font-size:12px} ++.bt-set-avatar{display:flex;justify-content:center;padding:4px 0} ++.bt-avatar-anchor{position:relative} ++.bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0} ++.bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)} ++.bt-avedit{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:300px;background:var(--bt-main);border:1px solid var(--bt-line);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.18);padding:10px;z-index:70;display:flex;flex-direction:column;gap:10px;color:var(--bt-ink);transform-origin:top center;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} ++.bt-bs .bt-avedit{transform-origin:32px 0} ++.bt-avedit-top{display:flex;align-items:center;justify-content:space-between} ++.bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px} ++.bt-avedit-tab{position:relative;z-index:1;border:0;background:none;border-radius:999px;padding:3px 10px;font-size:12px;line-height:16px;color:var(--bt-ink-2);cursor:pointer} ++.bt-avedit-tab[aria-selected=true]{background:var(--bt-main);color:var(--bt-ink)} ++.bt-avedit-tabs>.bt-thumb{top:2px;bottom:2px;border-radius:999px;background:var(--bt-main);box-shadow:0 1px 2px rgba(0,0,0,.06)} ++.bt-avedit-tabs[data-glide] .bt-avedit-tab[aria-selected=true]{background:transparent} ++.bt-avedit-reset{color:var(--bt-ink-3)} ++.bt-avedit-reset:hover{color:var(--bt-ink)} ++.bt-shapes{display:flex;flex-direction:column;gap:2px} ++.bt-shape-row{display:grid;grid-template-columns:repeat(6,1fr);gap:2px} ++.bt-shape{position:relative;aspect-ratio:1;border:0;border-radius:8px;background:none;color:var(--bt-ink);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0} ++.bt-shape:hover{background:var(--bt-hover)} ++.bt-shape .bt-ring{position:absolute;left:50%;top:50%;width:36px;height:36px;translate:-50% -50%;overflow:visible;opacity:0;scale:.82;pointer-events:none;transition:opacity .14s ease,${spring(SPRING_POP, "scale")}} ++.bt-shape[aria-pressed=true] .bt-ring{opacity:1;scale:1} ++.bt-colors{display:flex;justify-content:space-between} ++.bt-color{width:22px;height:22px;border-radius:50%;border:2px solid transparent;padding:2px;background:none;cursor:pointer;box-sizing:border-box;transition:border-color .14s ease,${spring(SPRING_TAP, "scale")}} ++.bt-color[aria-pressed=true] span{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++.bt-color span{display:block;width:100%;height:100%;border-radius:50%} ++.bt-color[aria-pressed=true]{border-color:var(--bt-ink)} ++.bt-dropzone{border:1.5px dashed var(--bt-line-2);border-radius:10px;padding:18px 12px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--bt-ink-2);font-size:12px;line-height:16px;text-align:center} ++.bt-dropzone[data-over]{border-color:var(--bt-accent);color:var(--bt-ink)} ++.bt-dropzone img{width:64px;height:64px;border-radius:50%;object-fit:cover} ++.bt-set-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center} ++.bt-about{white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden} ++.bt-about[data-open]{display:block;-webkit-line-clamp:unset} ++.bt-more{border:0;background:none;color:var(--bt-ink-3);font-size:12px;line-height:16px;cursor:pointer;padding:2px 0} ++.bt-more:hover{color:var(--bt-ink)} ++.bt-subhead{display:flex;align-items:center;gap:4px;padding:10px 12px 4px} ++.bt-subhead-title{flex:1;font-size:15px;line-height:24px;font-weight:600;padding-left:4px} ++.bt-icon-btn{width:28px;height:28px;border-radius:50%;border:0;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0} ++.bt-icon-btn:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-set-body{gap:20px} ++.bt-drawer-model{display:flex;flex-direction:column} ++.bt-drawer-model .bt-note{margin-top:6px} ++.bt-drawer-permission{display:flex;flex-direction:column} ++.bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135} ++ ++/* Settings dialog: a 198px page list beside the page. */ ++.bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:60;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-scrim{position:absolute;inset:0;background:rgba(0,0,0,.32);animation:bt-fade .22s ease-out} ++body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)} ++.bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))} ++:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none} ++:is(.bt-settings-layer,.bt-mem-layer)[data-leaving] .bt-scrim{opacity:0;transition:opacity .16s ease-in} ++:is(.bt-settings-layer,.bt-mem-layer)[data-leaving]>[role=dialog]{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards} ++@keyframes bt-dialog-rise{from{transform:translateY(12px) scale(.96)}} ++@keyframes bt-dialog-sink{to{opacity:0;transform:translateY(6px) scale(.975);filter:blur(3px)}} ++@media (prefers-reduced-motion:reduce){.bt-settings{transition:none}.bt-settings,.bt-scrim,.bt-settings-page>*{animation:bt-fade .12s ease-out!important}} ++.bt-settings-nav{position:relative;width:198px;flex:none;padding:16px 8px;border-right:.5px solid var(--bt-line);display:flex;flex-direction:column;gap:2px;background:var(--bt-sidebar);box-sizing:border-box} ++.bt-settings-item{position:relative;z-index:1;display:flex;align-items:center;gap:8px;padding:6px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left;transition:background-color .12s ease,color .12s ease} ++.bt-settings-item:hover{color:var(--bt-ink);background:var(--bt-hover)} ++.bt-settings-item[aria-current=page]{background:var(--bt-active);color:var(--bt-ink)} ++.bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} ++.bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} ++.bt-settings-shell{margin-top:auto} +.bt-settings-out{margin-left:auto;opacity:.6;font-size:12px} - /* Usage while another page is open: out of the flow at Usage's own width, so it keeps - its layout, and not rendered. */ - .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} -@@ -2288,6 +2288,8 @@ button.bt-set-row:hover{background:var(--bt-hover)} - .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} - .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} - .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} ++/* Usage while another page is open: out of the flow at Usage's own width, so it keeps ++ its layout, and not rendered. */ ++.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} ++/* The page column takes the dialog's final size at once, so while the dialog resizes ++ only its edge moves and the page does not lay out again every frame. */ ++.bt-settings-main{flex:none;display:flex;flex-direction:column;min-width:0;width:calc(var(--bt-set-w) - 198px);height:var(--bt-set-h)} ++.bt-settings-head{display:flex;align-items:center;gap:6px;padding:16px 56px 4px 20px} ++.bt-settings-head h2{margin:0;font-size:15px;line-height:24px;font-weight:600;animation:bt-veil-in .2s ease-out} ++.bt-settings-head>.bt-icon-btn{animation:bt-back-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} ++@keyframes bt-back-in{from{translate:6px 0;scale:.8}} ++.bt-settings-scroll{flex:1;overflow-y:auto;padding:12px 20px 24px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin} ++.bt-settings-page>:not(.bt-us){animation:bt-page-up ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .22s ease-out backwards} ++.bt-settings-page[data-move=in]>:not(.bt-us){animation-name:bt-page-in,bt-veil-in} ++.bt-settings-page[data-move=out]>:not(.bt-us){animation-name:bt-page-out,bt-veil-in} ++.bt-settings-page>:nth-child(2){animation-delay:30ms} ++.bt-settings-page>:nth-child(3){animation-delay:60ms} ++.bt-settings-page>:nth-child(4){animation-delay:90ms} ++.bt-settings-page>:nth-child(n+5){animation-delay:120ms} ++@keyframes bt-page-up{from{transform:translateY(10px)}} ++@keyframes bt-page-in{from{transform:translateX(18px)}} ++@keyframes bt-page-out{from{transform:translateX(-18px)}} ++.bt-settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0} ++.bt-settings-close:hover{background:var(--bt-active);color:var(--bt-ink)} ++.bt-settings-form{display:flex;flex-direction:column;gap:16px} ++.bt-set-section h3{font-size:13px;line-height:18px;font-weight:600;margin:0 0 8px;padding:0 4px} ++.bt-set-card{border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card);display:flex;flex-direction:column} ++.bt-set-card>*+*{border-top:.5px solid var(--bt-line)} ++.bt-set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;text-align:left;width:100%;box-sizing:border-box;border-radius:0} ++button.bt-set-row{cursor:pointer} ++button.bt-set-row:hover{background:var(--bt-hover)} ++.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} ++.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} ++.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} +/* #892: a plugin's rows in the Members card (Invite by link) sit on the card's own padding, like a row. */ +.bt-flybot-room-members{padding:10px 12px} - .bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0} - .bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))} - .bt-set-control{flex:none;display:flex;align-items:center;gap:6px} -@@ -2308,8 +2308,17 @@ button.bt-set-row:hover{background:var(--bt-hover)} - .bt-connector-actions{flex:none;display:flex;gap:6px} - .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} - .bt-connector-form-row{display:flex;align-items:center;gap:6px} ++.bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0} ++.bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))} ++.bt-set-control{flex:none;display:flex;align-items:center;gap:6px} ++.bt-set-value{font-size:13px;color:var(--bt-ink-2);text-align:right} ++.bt-set-bot{flex:1;display:flex;align-items:center;gap:8px;min-width:0} ++.bt-set-paste{flex-direction:column;align-items:stretch;gap:8px} ++.bt-chevron{color:var(--bt-ink-3);display:inline-flex;flex:none} ++ ++/* Settings → Connectors. */ ++.bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px} ++.bt-connector-head{display:flex;align-items:center;gap:12px;min-width:0} ++.bt-connector-mark{flex:none;width:36px;height:36px;border-radius:10px;background:var(--bt-card);border:.5px solid var(--bt-line);display:flex;align-items:center;justify-content:center;color:var(--bt-ink)} ++.bt-connector-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} ++.bt-connector-name{font-size:14px;line-height:20px;font-weight:500} ++.bt-connector-status{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-connector[data-state=ready] .bt-connector-status{color:var(--bt-ink-2)} ++.bt-connector[data-state=error] .bt-connector-status{color:#e02135;white-space:normal} ++.bt-connector-actions{flex:none;display:flex;gap:6px} ++.bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-connector-form-row{display:flex;align-items:center;gap:6px} +.bt-connector-apps{display:flex;flex-direction:column;gap:8px} - .bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)} ++.bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)} +.bt-connector-uses{padding-left:48px;font-size:12px;line-height:16px;color:var(--bt-ink-3)} +.bt-connector-bots{display:flex;flex-direction:column;gap:2px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +.bt-connector-others{padding-left:20px} - .bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135} ++.bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135} +.bt-connector-app-list{list-style:none;margin:0;padding:0 0 0 48px;display:flex;flex-direction:column;gap:2px} +.bt-connector-app{display:flex;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-2)} +.bt-connector-app-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.bt-app-status{color:var(--bt-ink-3)} +[data-status=sign-in]>.bt-app-status{color:#c46a00} - - /* Shell chrome reshaped for the team view. Selectors use stable data attributes where the - shell offers them and CSS-module name suffixes elsewhere. */ -@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side - .bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} - .bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} - @media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} --.bt-account-menu{width:224px} ++ ++/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the ++ shell offers them and CSS-module name suffixes elsewhere. */ ++[class*="_logoRow"],[class*="_newSession"],nav[class*="_panelList"]{display:none!important} ++[class*="_footArea"]{display:flex!important;flex-direction:row-reverse;align-items:center;gap:9px;padding:8px 6px 6px!important} ++[class*="_footArea"]>[class*="_footerActions"]{flex:1 1 auto;min-width:0} ++[class*="_footArea"]>[class*="_settingsArea"]{flex:none;width:auto!important} ++[class*="_footArea"] button[aria-label="Settings"]{width:auto!important;padding:0!important;background:none!important;border-radius:50%!important;height:auto!important} ++[class*="_widthHandle"]{display:none!important} ++ ++.bt-voice-pane{flex-direction:column;align-items:center;justify-content:center;animation:bt-fade .2s ease-out} ++.bt-voice-stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:24px;max-width:560px;text-align:center} ++.bt-voice-orb{position:relative;width:200px;height:200px;display:grid;place-items:center;margin-bottom:8px} ++.bt-voice-ring{position:absolute;inset:34px;border-radius:50%;background:var(--bt-hover);transform:scale(calc(1 + var(--bt-level,0) * .55));transition:transform .08s linear} ++.bt-voice-ring-2{inset:14px;background:none;box-shadow:inset 0 0 0 1px var(--bt-line-2);transform:scale(calc(.94 + var(--bt-level,0) * .3));opacity:.8} ++.bt-voice-stage[data-phase=thinking] .bt-voice-ring{animation:bt-voice-breathe 1.6s ease-in-out infinite} ++.bt-voice-stage[data-phase=speaking] .bt-voice-ring{animation:bt-voice-speak .9s ease-in-out infinite} ++@keyframes bt-voice-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}} ++@keyframes bt-voice-speak{0%,100%{transform:scale(1.04)}30%{transform:scale(1.2)}60%{transform:scale(1.1)}} ++.bt-voice-orb>.bt-mark{position:relative} ++.bt-voice-name{font-size:16px;line-height:22px;font-weight:500} ++.bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px} ++.bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} ++.bt-voice-note{font-size:12px;color:#e02135} ++.bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease} ++.bt-voice-setup:hover{background:var(--bt-active)} ++.bt-voice-bar{display:flex;gap:16px;padding:0 0 32px} ++.bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease} ++.bt-voice-btn:hover{background:var(--bt-active)} ++.bt-voice-btn:active{transform:scale(.94)} ++.bt-voice-btn[aria-pressed=true]{background:var(--bt-user);color:var(--bt-user-ink)} ++.bt-voice-slash{position:absolute;width:24px;height:2px;border-radius:1px;background:currentColor;transform:rotate(-45deg)} ++.bt-voice-end{background:#ff3e51;color:#fff} ++.bt-voice-end:hover{background:#e02135} ++@keyframes bt-fade{from{opacity:0}to{opacity:1}} ++@keyframes bt-blur-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}} ++@media (prefers-reduced-motion:reduce){.bt-voice-pane,.bt-voice-caption,.bt-voice-ring{animation:none!important}} ++.bt-msg{display:flex;flex-direction:column;align-items:flex-start;max-width:min(80%,560px,calc(100% - 82px))} ++/* The bubble's row: reactions hang below it, so the hover toolbar (and a group ++ sender's avatar) line up with the bubble alone. */ ++.bt-msg-line{position:relative;display:flex;align-items:flex-end;gap:8px;max-width:100%} ++.bt-msg-line>.bt-bubble{max-width:none;min-width:0} ++/* The same bubble width as beside an avatar column: 80% of (100% - 30px), plus the 30px. */ ++.bt-astack[data-lead]>.bt-msg{max-width:min(calc(80% + 6px),590px,calc(100% - 52px))} ++.bt-tools{position:absolute;top:50%;left:100%;margin-left:6px;transform:translateY(-50%);display:flex;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s} ++.bt-tools::after{content:"";position:absolute;top:-8px;bottom:-8px;right:100%;width:128px;z-index:-1} ++.bt-msg:hover>.bt-msg-line>.bt-tools,.bt-msg:focus-within>.bt-msg-line>.bt-tools,.bt-tools[data-open]{opacity:1;visibility:visible} ++.bt-tool{width:24px;height:24px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease} ++.bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-tool:active{background:var(--bt-active)} ++.bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} ++.bt-react-strip{position:fixed;z-index:60;pointer-events:auto;display:flex;gap:2px;padding:4px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.08),0 4px 6px -4px rgba(0,0,0,.06);transform:translate(-50%,-100%);transform-origin:bottom center;animation:bt-pop-lift ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} ++.bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} ++.bt-react-strip button:nth-child(2){animation-delay:20ms} ++.bt-react-strip button:nth-child(3){animation-delay:40ms} ++.bt-react-strip button:nth-child(4){animation-delay:60ms} ++.bt-react-strip button:nth-child(n+5){animation-delay:80ms} ++.bt-react-strip button:hover{background:var(--bt-hover);transform:scale(1.15)} ++.bt-react-strip button:active{transform:scale(.92)} ++.bt-reacts{display:flex;gap:4px;margin-top:4px} ++.bt-react{height:22px;min-width:30px;padding:0 6px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-main);font-size:13px;line-height:20px;cursor:pointer;animation:bt-react-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing};transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} ++.bt-react:hover{background:var(--bt-hover)} ++.bt-react:active{scale:.92} ++@keyframes bt-react-pop{from{transform:scale(.6);opacity:0}} ++.bt-bubble a[href^="${MENTION_ORIGIN}"] svg{display:none} ++.bt-you{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,var(--bt-sidebar));color:var(--bt-ink-2);box-shadow:inset 0 0 0 1px var(--bt-line);transition:background-color .12s ease} ++button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} ++.bt-you-wrap{display:inline-flex;padding:0} ++.bt-connect-foot{width:100%;height:36px} ++ ++.bt-menu{position:fixed;pointer-events:auto;z-index:60;min-width:200px;padding:6px;border-radius:12px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1),0 0 0 1px rgba(228,228,228,.04);display:flex;flex-direction:column;gap:2px;transform-origin:var(--bt-origin,top left);animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box} ++.bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box} ++.bt-menu button:hover{background:var(--bt-hover)} ++.bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px} ++.bt-menu .bt-danger{color:#c21d2e} ++.bt-menu-sep{height:.5px;flex:none;background:var(--bt-line-2);margin:4px 8px} ++.bt-menu-up{--bt-origin:bottom left;animation-name:bt-pop-lift,bt-veil-in} ++.bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards} ++.bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums} ++.bt-palette-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:70;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out} ++.bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-palette-layer[data-leaving]{pointer-events:none} ++.bt-palette-layer[data-leaving] .bt-backdrop{opacity:0;transition:opacity .16s ease-in} ++.bt-palette-layer[data-leaving] .bt-palette{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards} ++@keyframes bt-palette-in{from{translate:0 -10px;scale:.96}} ++.bt-palette-head{display:flex;align-items:center;gap:4px;padding:14px 10px 14px 14px;border-bottom:.5px solid var(--bt-line-2)} ++.bt-palette-glyph{display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;color:var(--bt-ink-3);margin-right:4px} ++.bt-palette-head input{flex:1;min-width:0;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0} ++.bt-palette-head input::placeholder{color:var(--bt-ink-3)} ++.bt-palette-clear{width:18px;height:18px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer} ++.bt-palette-clear:hover{color:var(--bt-ink)} ++.bt-palette-clear svg{width:12px;height:12px} ++.bt-palette-list{position:relative;height:402px;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px;box-sizing:border-box} ++.bt-palette-list>.bt-thumb,.bt-drop-list>.bt-thumb{left:0;right:0;border-radius:8px;background:var(--bt-active)} ++.bt-palette-list>.bt-thumb{left:8px;right:8px} ++:is(.bt-palette-list,.bt-drop-list)[data-glide] [aria-selected=true]{background:transparent} ++:is(.bt-palette-row,.bt-palette-section,.bt-drop-list>.bt-option){position:relative;z-index:1} ++.bt-palette-section{padding:8px 8px 4px;font-size:12px;line-height:16px;color:var(--bt-ink-3);flex:none} ++.bt-palette-section:first-of-type{padding-top:0} ++.bt-palette-row{flex:none;display:flex;align-items:center;gap:8px;height:49px;padding:6px 10px 6px 8px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;box-sizing:border-box;width:100%} ++.bt-palette-row[aria-selected=true]{background:var(--bt-active)} ++.bt-palette-lead{flex:none;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center} ++.bt-palette-cmd{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} ++.bt-palette-cmd svg{width:14px;height:14px} ++.bt-palette-agent{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink)} ++.bt-palette-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} ++.bt-palette-title{font-size:14px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-palette-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-palette-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink)} ++.bt-palette-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:0 16px;text-align:center;animation:bt-veil-in .22s ease-out} ++.bt-palette-empty-icon{color:var(--bt-line-2)} ++.bt-palette-empty-icon svg{width:32px;height:32px} ++.bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} ++.bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} ++@media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} +.bt-account-menu{min-width:224px;width:max-content;max-width:calc(100vw - 16px)} +.bt-flybot-account-header:not(:empty){padding:6px 0 2px;margin-bottom:4px;border-bottom:.5px solid var(--bt-line-2)} - .bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} - .bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} - /* Popover keyframes move translate and scale, which compose with a transform the popover -@@ -2599,6 +2599,14 @@ var CONVERSATION_CSS = ` - .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} - .bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} - .bt-think summary::-webkit-details-marker{display:none} ++.bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} ++.bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} ++/* Popover keyframes move translate and scale, which compose with a transform the popover ++ already has. */ ++@keyframes bt-pop-drop{from{translate:0 -4px;scale:.96}} ++@keyframes bt-pop-lift{from{translate:0 4px;scale:.96}} ++@keyframes bt-pop-out{to{opacity:0;scale:.97;filter:blur(2px)}} ++@keyframes bt-veil-in{from{opacity:0;filter:blur(6px)}} ++@keyframes bt-check-pop{from{opacity:0;scale:.4}} ++@media (prefers-reduced-motion:reduce){.bt-menu,.bt-avedit,.bt-react-strip{animation:bt-fade .1s ease-out}.bt-react-strip button,.bt-q-check,.bt-color[aria-pressed=true] span{animation:none}} ++.bt-brand:hover{background:var(--bt-hover)} ++.bt-drop-list{position:relative;display:flex;flex-direction:column;gap:2px} ++.bt-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px} ++.bt-pick{display:flex;flex-direction:column;align-items:center;gap:4px;padding:7px 2px 6px;border-radius:10px;border:1px solid transparent;background:none;color:var(--bt-ink);cursor:pointer;font:inherit;font-size:11px;line-height:14px;min-width:0} ++.bt-pick:hover{background:var(--bt-hover)} ++.bt-pick[aria-pressed=true]{border-color:var(--bt-line-2);background:var(--bt-hover)} ++.bt-pick>span:last-child:not(.bt-mark){max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-pick:hover .bt-mark[data-state=idle] .bt-eye{animation:bt-blink .5s linear 1} ++.bt-select{width:100%;height:34px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink);padding:0 8px;font:inherit;font-size:13px} ++.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:6px} ++.bt-swatch-sep{width:1px;height:22px;background:var(--bt-line-2);margin:0 2px} ++.bt-swatch-custom{position:relative;overflow:hidden;background:conic-gradient(#ff3e51,#ffaf38,#00c972,#1cc3b0,#2a92fe,#a97efe,#ff5eb1,#ff3e51);box-sizing:border-box} ++.bt-swatch-custom[data-on=true]{border-color:var(--bt-ink)} ++.bt-swatch-custom input{position:absolute;inset:-4px;width:calc(100% + 8px);height:calc(100% + 8px);opacity:0;cursor:pointer;border:0;padding:0} ++.bt-swatch-default{background:linear-gradient(135deg,var(--bt-card) 0 46%,var(--bt-line-2) 46% 54%,var(--bt-card) 54%);box-shadow:inset 0 0 0 1px var(--bt-line-2)} ++.bt-states{display:flex;flex-wrap:wrap;gap:4px} ++.bt-state{border:1px solid var(--bt-line);background:none;border-radius:999px;font:inherit;font-size:11px;line-height:16px;padding:2px 8px;color:var(--bt-ink-2);cursor:pointer} ++.bt-state:hover{color:var(--bt-ink)} ++.bt-state[aria-pressed=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} ++.bt-theme-chip{position:relative;width:46px;height:30px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--bt-line-2);background:linear-gradient(90deg,var(--ls) 0 36%,var(--lb) 36%);overflow:hidden} ++.bt-theme-chip i{position:absolute;right:7px;bottom:7px;width:10px;height:10px;border-radius:50%;background:var(--la)} ++body[data-ds-dark-theme] .bt-theme-chip{background:linear-gradient(90deg,var(--ds) 0 36%,var(--db) 36%)} ++body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} ++.bt-tabs{flex-wrap:wrap} ++.bt-soft:disabled{opacity:.4;cursor:default} ++.bt-group-role{margin-left:6px;font-size:11px;line-height:14px;padding:1px 5px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);vertical-align:1px} ++.bt-members{display:flex;flex-direction:column;gap:1px} ++.bt-member{display:flex;align-items:center;gap:4px;min-height:40px;padding:0 4px 0 6px;border-radius:8px;animation:bt-rise .18s ease-out} ++.bt-member:hover{background:var(--bt-hover)} ++.bt-member-name{flex:1;min-width:0;display:flex;align-items:center;gap:10px;border:0;background:none;padding:6px 0;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;cursor:pointer;text-align:left} ++.bt-member-name>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-chip-admin{background:var(--bt-tag-bg);color:var(--bt-tag-ink);border-color:transparent} ++.bt-mini{flex:none;border:0;background:none;border-radius:6px;padding:2px 6px;font:inherit;font-size:12px;line-height:18px;color:var(--bt-ink-2);cursor:pointer} ++.bt-mini:hover{background:var(--bt-active);color:var(--bt-ink)} ++.bt-member .bt-mini,.bt-member .bt-x{opacity:0;transition:opacity .15s ease} ++.bt-member:hover .bt-mini,.bt-member:hover .bt-x,.bt-member .bt-mini:focus-visible,.bt-member .bt-x:focus-visible{opacity:1} ++.bt-add{color:var(--bt-ink-2);margin-top:2px} ++.bt-add:hover,.bt-addlist .bt-option:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-addlist{display:flex;flex-direction:column;margin-top:4px;padding:4px;border:1px solid var(--bt-line);border-radius:10px;background:var(--bt-card);animation:bt-rise .16s ease-out} ++.bt-seg{position:relative;display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--bt-hover)} ++.bt-motion-seg{width:216px} ++.bt-seg button{position:relative;z-index:1;flex:1;height:30px;border:0;border-radius:8px;background:none;font:inherit;font-size:13px;color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,box-shadow .15s ease,${spring(SPRING_TAP, "scale")}} ++.bt-seg button[aria-pressed=true]{background:var(--bt-card);color:var(--bt-ink);box-shadow:0 1px 3px rgba(0,0,0,.08)} ++.bt-seg button:disabled{opacity:.4;cursor:default} ++.bt-seg button:active:not(:disabled){scale:.96} ++.bt-seg>.bt-thumb{top:2px;bottom:2px;border-radius:8px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08)} ++.bt-seg[data-glide] button[aria-pressed=true]{background:transparent;box-shadow:none} ++@keyframes bt-rise{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}} ++@media (prefers-reduced-motion:reduce){.bt-member,.bt-addlist{animation:none}} ++ ++/* Memory dialog: a summary of every entry, the entries behind the menu, "Ask or update" below. */ ++.bt-mem-row{display:flex;align-items:center;gap:8px;width:100%;height:36px;padding:0 8px 0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;box-sizing:border-box;transition:background-color .12s ease} ++.bt-mem-row:hover{background:var(--bt-hover)} ++.bt-mem-row>svg{flex:none;color:var(--bt-ink-2)} ++.bt-mem-row-copy{flex:1;text-align:left} ++.bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px} ++.bt-mem-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:61;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-mem-scrim{animation:bt-fade .2s ease-out} ++.bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box} ++.bt-mem-head .bt-icon-btn{width:32px;height:32px;flex:none} ++.bt-mem-head [aria-haspopup] svg{width:18px;height:18px;stroke-width:2.6} ++.bt-mem-head>.bt-icon-btn:first-child{margin-left:-10px} ++.bt-mem-head h2{margin:0;font-size:17px;line-height:24px;font-weight:600;white-space:nowrap} ++.bt-mem-status{font-size:13px;line-height:18px;color:var(--bt-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-variant-numeric:tabular-nums} ++.bt-mem-head-gap{flex:1} ++.bt-mem-menu{position:absolute;top:52px;right:50px;min-width:220px;--bt-origin:top right} ++.bt-mem-menu button:disabled{opacity:.4;cursor:default;background:none} ++.bt-mem-body{flex:1;overflow-y:auto;padding:22px 28px 24px;scrollbar-width:thin;display:flex;flex-direction:column;gap:12px} ++.bt-mem-summary{transition:opacity .3s ease} ++.bt-mem-summary[data-stale]{opacity:.55} ++.bt-mem-summary h3{margin:20px 0 6px;font-size:17px;line-height:26px;font-weight:600} ++.bt-mem-summary h3:first-child{margin-top:0} ++.bt-mem-summary p{margin:0;font-size:15px;line-height:26px} ++.bt-mem-summary>*{animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both} ++.bt-mem-summary>:nth-child(2){animation-delay:.03s} ++.bt-mem-summary>:nth-child(3){animation-delay:.06s} ++.bt-mem-summary>:nth-child(4){animation-delay:.09s} ++.bt-mem-summary>:nth-child(5){animation-delay:.12s} ++.bt-mem-summary>:nth-child(6){animation-delay:.15s} ++.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} ++@keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} ++.bt-mem-skel{display:flex;flex-direction:column;gap:24px} ++.bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} ++.bt-mem-skel-block span{display:block;height:12px;border-radius:6px;background:linear-gradient(90deg,var(--bt-hover) 25%,var(--bt-active) 50%,var(--bt-hover) 75%);background-size:200% 100%;animation:bt-mem-skel 1.4s ease-in-out infinite} ++.bt-mem-skel-block .bt-mem-skel-head{width:30%;height:16px;margin-bottom:2px} ++@keyframes bt-mem-skel{from{background-position:150% 0}to{background-position:-50% 0}} ++.bt-mem-skel-label{font-size:13px;line-height:18px;color:var(--bt-ink-3)} ++.bt-mem-empty{margin:auto;max-width:400px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;padding:24px 0;animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both} ++.bt-mem-empty-mark{width:48px;height:48px;border-radius:50%;background:var(--bt-hover);display:flex;align-items:center;justify-content:center;color:var(--bt-ink-2);margin-bottom:4px} ++.bt-mem-empty-mark svg{width:22px;height:22px} ++.bt-mem-empty-title{font-size:15px;line-height:22px;font-weight:600} ++.bt-mem-empty-text{font-size:13px;line-height:20px;color:var(--bt-ink-2)} ++.bt-mem-failed{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;background:var(--bt-hover);font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-mem-failed>span{flex:1;min-width:0;overflow-wrap:anywhere} ++.bt-mem-entries{display:flex;flex-direction:column;gap:28px;animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .22s ease-out} ++.bt-mem-section-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px} ++.bt-mem-section-head h3{margin:0;font-size:15px;line-height:22px;font-weight:600} ++.bt-mem-section-head span{font-size:12px;line-height:16px;color:var(--bt-ink-3)} ++.bt-mem-section-head .bt-mem-size{margin-left:auto;font-variant-numeric:tabular-nums;white-space:nowrap} ++.bt-mem-topic{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin:14px 0 2px 4px;font-weight:500} ++.bt-mem-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column} ++.bt-mem-entry{display:flex;align-items:center;gap:10px;padding:9px 4px;border-bottom:.5px solid var(--bt-line);transition:opacity .2s ease} ++.bt-mem-entry[data-busy]{opacity:.4} ++.bt-mem-entry-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} ++.bt-mem-entry-text{font-size:14px;line-height:20px;overflow-wrap:anywhere} ++.bt-mem-entry-meta{font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums} ++.bt-mem-remove{flex:none;height:28px;min-width:28px;border-radius:999px;border:0;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0 6px;font:inherit;font-size:12px;opacity:0;transition:opacity .12s ease,background-color .12s ease,color .12s ease} ++.bt-mem-entry:hover .bt-mem-remove,.bt-mem-remove:focus-visible,.bt-mem-remove[data-confirm]{opacity:1} ++.bt-mem-remove:hover{background:var(--bt-hover);color:var(--bt-ink)} ++.bt-mem-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135;padding:0 10px} ++@media (hover:none){.bt-mem-remove{opacity:1}} ++.bt-mem-none{font-size:13px;line-height:18px;color:var(--bt-ink-3);padding:8px 4px} ++.bt-mem-foot{flex:none;padding:8px 20px 20px;display:flex;flex-direction:column;gap:10px} ++.bt-mem-reply{border-radius:16px;background:var(--bt-hover);padding:8px 8px 12px 16px;display:flex;flex-direction:column;gap:6px;max-height:36vh;overflow-y:auto;scrollbar-width:thin;transform-origin:50% 100%;animation:bt-pop-lift ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} ++.bt-mem-reply-head{display:flex;align-items:center;gap:8px;min-height:24px} ++.bt-mem-reply-asked{flex:1;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-mem-reply .bt-icon-btn{width:24px;height:24px;flex:none} ++.bt-mem-reply-text{font-size:14px;line-height:21px;padding-right:8px;overflow-wrap:anywhere} ++.bt-mem-changes{list-style:none;margin:2px 0 0;padding:0 8px 0 0;display:flex;flex-direction:column;gap:4px} ++.bt-mem-changes li{display:flex;align-items:flex-start;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2);overflow-wrap:anywhere} ++.bt-mem-changes li svg{flex:none;margin-top:1px;color:#e02135;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} .12s backwards} ++.bt-mem-changes li[data-ok] svg{color:var(--bt-green)} ++.bt-mem-ask{display:flex;align-items:center;gap:8px;height:52px;padding:0 8px 0 22px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-card);box-shadow:0 4px 16px rgba(0,0,0,.06);box-sizing:border-box;transition:border-color .15s ease,box-shadow .15s ease} ++.bt-mem-ask:focus-within{border-color:var(--bt-ink-3);box-shadow:0 4px 20px rgba(0,0,0,.1)} ++.bt-mem-input{flex:1;min-width:0;border:0;background:none;outline:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0} ++.bt-mem-input::placeholder{color:var(--bt-ink-3)} ++.bt-mem-send{flex:none;width:36px;height:36px;border-radius:50%;border:0;background:var(--bt-ink);color:var(--bt-main);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")}} ++.bt-mem-send:disabled{opacity:.22;cursor:default} ++.bt-mem-send:not(:disabled):active{scale:.92} ++.bt-mem-send>*{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++.bt-mem-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;box-sizing:border-box;animation:bt-mem-spin .7s linear infinite} ++@keyframes bt-mem-spin{to{transform:rotate(360deg)}} ++@media (max-width:640px){.bt-mem{width:100vw;height:100%;border-radius:0;border:0}.bt-mem-body{padding:18px 18px 20px}.bt-mem-foot{padding:8px 12px 12px}} ++@media (prefers-reduced-motion:reduce){.bt-mem,.bt-mem-scrim,.bt-mem-summary>*,.bt-mem-empty,.bt-mem-reply,.bt-mem-skel-block span,.bt-mem-entries,.bt-mem-send>*,.bt-mem-changes li svg{animation:none}} ++ ++.bt-thumb{position:absolute;left:0;top:0;z-index:0;pointer-events:none;transition:opacity .14s ease} ++.bt-thumb[data-hidden]{opacity:0} ++[data-morph]{overflow:hidden!important} ++.bt-veil{animation:bt-veil-in .22s ease-out} ++.bt-with-icon{display:inline-flex;align-items:center;gap:4px} ++.bt-with-icon svg{width:14px;height:14px} ++.bt-tag-admin{display:inline-flex;align-items:center;gap:4px} ++.bt-create-inline{padding:18px 16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} ++.bt-create-inline .bt-create-card{max-width:none} ++.bt-panel-view>[data-move=in]{animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} ++.bt-panel-view>[data-move=out]{animation:bt-page-out ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} ++.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(2){animation-delay:25ms} ++.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(n+3){animation-delay:50ms} ++.bt-panel-view>.bt-drawer-body[data-move=tab]{animation:bt-veil-in .2s ease-out} ++.bt-drawer-name-input{animation:bt-veil-in .16s ease-out} ++.bt-pop-in{display:inline-flex;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++:is(.bt-soft,.bt-send,.bt-icon-btn,.bt-settings-close,.bt-mini,.bt-tab,.bt-nc-send,.bt-avedit-tab,.bt-shape,.bt-pick,.bt-swatch,.bt-palette-clear){transition:background-color .12s ease,color .12s ease,border-color .12s ease,opacity .12s ease,${spring(SPRING_TAP, "scale")}} ++:is(.bt-icon-btn,.bt-settings-close,.bt-nc-send,.bt-shape,.bt-swatch,.bt-color,.bt-x,.bt-q-x,.bt-palette-clear):active:not(:disabled){scale:.9} ++:is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} ++@media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} ++`; ++var CONVERSATION_CSS = ` ++.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} ++.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} ++.bt-think summary::-webkit-details-marker{display:none} +.bt-think-caret{display:inline-block;transition:transform .15s;font-size:14px;line-height:1} +.bt-think[open] .bt-think-caret{transform:rotate(90deg)} +.bt-person-mark{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-ink-5,rgba(20,20,20,.08));color:var(--bt-ink-3,rgba(20,20,20,.45))} @@ -2877,27 +4364,28 @@ diff --git a/client.js b/client.js +/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */ +.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line} +.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))} - .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} - .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} - .bt-event>span{padding:0 2px} -@@ -2612,7 +2612,6 @@ var CONVERSATION_CSS = ` - .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} - [class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} --.bt-activity{display:flex;align-items:center;gap:10px;margin:8px 0 4px 2px;font-size:14px;color:var(--bt-ink-2)} - .bt-group-replies{display:contents} - /* A Bot chat is flat in every work-details mode, without changing the user's mode - (it is a DSH-wide setting): step groups lose their header and stay open, and what a -@@ -2622,21 +2622,28 @@ var CONVERSATION_CSS = ` - [data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} - :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} - [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} --[class*="_header"]:has(.bt-head){border-bottom-color:transparent!important;box-shadow:none!important} --/* The transcript runs under a floating header pill. */ --header[class*="_header"]:has(.bt-head){position:absolute!important;top:0;left:0;right:0;z-index:8;display:flex!important;padding:10px 0 0!important;background:transparent!important;pointer-events:none;justify-content:center} --header[class*="_header"]:has(.bt-head)::before{content:"";position:absolute;inset:0 0 auto;height:60px;background:linear-gradient(to bottom,var(--bt-main),color-mix(in srgb,var(--bt-main) 0%,transparent));pointer-events:none;z-index:-1} --header[class*="_header"]:has(.bt-head)>[class*="_headerLeading"]{position:absolute;left:12px;top:10px;pointer-events:auto} --header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} ++.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} ++.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} ++.bt-event>span{padding:0 2px} ++.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease} ++.bt-event button:hover{background:var(--bt-hover)} ++.bt-event button:active{background:var(--bt-active)} ++.bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px} ++.bt-event-whole>*{flex:none} ++.bt-event-whole>span:first-child{margin-right:2px} ++/* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */ ++.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} ++[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} ++.bt-group-replies{display:contents} ++/* A Bot chat is flat in every work-details mode, without changing the user's mode ++ (it is a DSH-wide setting): step groups lose their header and stay open, and what a ++ mode folds away in a finished turn stays shown. TurnProcessCell also opens that ++ fold; these rules cover the first frame before it does. */ ++[data-step-process]>div:first-child{display:none!important} ++[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} ++:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} ++[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} +/* FlyBot's pill rides the shell's own Session header utilities, beside the seats + the native header renders (actions: the subagent catalog; lineage; corner). + It is a compact control in that row now, not a floating replacement for the @@ -2910,141 +4398,355 @@ diff --git a/client.js b/client.js +[class*="_header"] [class*="switcherTitle"]{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} +[class*="_header"] [class*="switcherTrigger"]{height:auto;align-items:flex-start} +[class*="_header"] .bt-head .bt-mark{width:22px!important;height:22px!important;--bt-size:22px} - [class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)} - /* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators), - without its copy and branch row; a tail with nothing in it takes no room. */ --[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]){display:none!important} --[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))){display:none!important} ++[class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)} ++/* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators), ++ without its copy and branch row; a tail with nothing in it takes no room. */ +[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]):not(.bt-typing){display:none!important} +[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))):not(:has([data-turn-tail]>.bt-typing)){display:none!important} - /* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not - the shell's new-session hero: no headline, no workspace or mode picker, and the - composer stays docked at the bottom. */ - [data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important} ++/* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not ++ the shell's new-session hero: no headline, no workspace or mode picker, and the ++ composer stays docked at the bottom. */ ++[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important} +body[data-bt-remote-chat=empty][data-content-phase=hero] [class*="_scrollBody"]::before{content:var(--bt-remote-empty);display:block;text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:16px 12px 8px;max-width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto;white-space:pre-wrap} - [data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} - [data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} - [data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} -@@ -2648,6 +2648,9 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} - .bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards} - .bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards} ++[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} ++[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} ++[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} ++/* The right sides give back the scrollbar gutter so both edges sit 16px from the column. */ ++[class*="_scroll"]:has(> [class*="_column"]){padding:60px 11px 16px 16px!important} ++/* The shell's running status only hosts the activity row (.bt-typing). */ ++[data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} ++[data-chat-running]>span:not([role=status]){display:none!important} ++[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} ++.bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards} ++.bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards} +.bt-typing[data-carry],.bt-typing[data-carry]>.bt-typing-text{animation:none} +/* Between a friend's chat turns the row sits in the last turn's tail (#443), at the transcript's foot. */ +[data-turn-tail]>.bt-typing{margin-top:var(--dsh-chat-flow-gap,12px)} - .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} - .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} - .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} -@@ -2679,7 +2679,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - @keyframes bt-fade-in{from{opacity:0}to{opacity:1}} - @keyframes bt-fade-out{from{opacity:1}to{opacity:0}} - [data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} --[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} ++.bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} ++.bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} ++.bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} ++.bt-typing>.bt-mark .bt-eyes{transition:scale .28s cubic-bezier(.4,.06,.18,1)} ++.bt-typing>.bt-mark:hover .bt-eyes{scale:1.18} ++.bt-typing-text{display:flex;align-items:center;flex:1 1 auto;min-width:0;animation:bt-typing-label .22s ease-out .2s backwards} ++.bt-typing-label{display:inline-flex;align-items:center;gap:6px;min-width:0;animation:bt-typing-label .22s ease-out backwards} ++.bt-typing-label>.bt-shimmer{font-size:14px;line-height:22px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-typing-time{flex-shrink:0;font-size:12px;line-height:22px;color:var(--bt-ink-3);white-space:nowrap} ++@keyframes bt-typing-in{from{opacity:0;transform:scale(.9)}} ++@keyframes bt-typing-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.92);filter:blur(3px)}} ++@keyframes bt-typing-label{from{opacity:0;filter:blur(5px);transform:translateY(3px)}} ++/* Shimmer: a stepped sweep clipped to the text. */ ++.bt-shimmer{color:transparent;background:linear-gradient(90deg,var(--bt-shimmer-base) 0%,var(--bt-shimmer-base) 25%,var(--bt-ink) 60%,var(--bt-shimmer-base) 75%,var(--bt-shimmer-base) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;animation:bt-shimmer 2.2s steps(60) infinite;--bt-shimmer-base:color-mix(in srgb,var(--bt-ink) 40%,transparent)} ++@keyframes bt-shimmer{from{background-position:200% 0}to{background-position:-200% 0}} ++/* New rows slide up; a reply that replaces the activity row grows out of its corner. */ ++[data-bt-enter="new"]{animation:bt-row-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} ++[data-bt-enter="after-collapse"]{transform-origin:0 100%;animation:bt-row-after .24s cubic-bezier(.23,1,.32,1) .38s backwards} ++@keyframes bt-row-in{from{transform:translateY(12px)}to{transform:none}} ++@keyframes bt-row-after{0%{opacity:0;transform:translateY(12px) scale(.94)}55%{opacity:1}100%{opacity:1;transform:none}} ++@media (prefers-reduced-motion:reduce){ ++.bt-typing,.bt-typing[data-exiting]{animation-name:bt-fade-in;animation-duration:.12s;transform:none} ++.bt-typing[data-exiting]{animation-name:bt-fade-out} ++.bt-typing-text,.bt-typing-label{animation-name:bt-fade-in;animation-delay:0s} ++.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)} ++[data-bt-enter="new"]{animation:none} ++[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out} ++} ++@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} ++@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} ++[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +[data-composer-card]:hover{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} +[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 55%,transparent)!important} - [data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} - [class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} - [class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} -@@ -2691,6 +2691,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - [data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} - [data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} - [data-composer-placeholder]{font-size:0!important;line-height:0!important} ++[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} ++[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} ++[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} ++[data-composer-card]>[class*="_row"]{display:contents!important} ++[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} ++[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} ++[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} ++[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} ++[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} ++[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} ++[data-composer-placeholder]{font-size:0!important;line-height:0!important} +/* A friend's Bot chat: no local run stats (they are relay turns, always 0; #419). */ +body[data-bt-friend-chat] [data-composer-stats]{display:none!important} - [data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} - /* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ - [data-composer-card] button[class*="_add"]{display:none!important} -@@ -2722,6 +2722,7 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - .bt-mic:active,.bt-voice:active{scale:.9} - [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} - body[data-bt-room] .bt-voice{display:none!important} ++[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} ++/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ ++[data-composer-card] button[class*="_add"]{display:none!important} ++.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} ++.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} ++.bt-plus:active{scale:.9} ++.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} ++.bt-plus[aria-expanded=true] svg{rotate:45deg} ++.bt-plus-menu{min-width:180px} ++[data-composer-card] [class*="_tools"]{gap:0!important} ++[data-composer-card] [class*="_modes"]:empty{display:none!important} ++[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} ++[data-composer-card] [class*="_trailing"]{gap:8px} ++[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} ++[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} ++[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} ++@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} ++[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} ++[data-composer-card] button[class*="_primary"]:disabled{display:none!important} ++[data-composer-dock]{display:none!important} ++.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} ++.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} ++.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} ++.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} ++@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} ++.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} ++.bt-voice:hover{opacity:.9} ++.bt-mic:active,.bt-voice:active{scale:.9} ++[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} ++body[data-bt-room] .bt-voice{display:none!important} +body[data-bt-room] [data-composer-card] [class*="_activity"]{display:none!important} - body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} - .bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} - [data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} -@@ -2776,6 +2776,7 @@ div:has(> nav [class*="_marks"]){display:none!important} - .bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} - .bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} - .bt-urow{display:flex;justify-content:flex-end} ++body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} ++.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} ++[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} ++.bt-q{order:1} ++div:has(> nav [class*="_marks"]){display:none!important} ++/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) ++ takes its surface and its text from the one --bt-user pair: a skin that repaints ++ the host's bubble surface alone would otherwise leave the text unreadable. */ ++[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} ++[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} ++/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ ++[class*="_userRow"] [class*="_actions"]{display:none!important} ++[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} ++[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} ++[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} ++[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} ++[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} ++[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} ++[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} ++.bt-q-inline{display:flex;margin:4px 0 6px} ++[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} ++[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} ++.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} ++.bt-q-dock{width:100%;display:flex;justify-content:center} ++.bt-q-dock .bt-q{margin:0 auto 8px} ++[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} ++[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} ++[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} ++[class*="_toBottomSlot"]>button:active{scale:.9} ++[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} ++[class*="_toBottomSlot"]>button svg{display:none} ++[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} ++@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} ++[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} ++.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} ++@keyframes bt-news-in{from{translate:0 8px;scale:.9}} ++.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} ++@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} ++.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} ++.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} ++.bt-news-ico svg{width:16px;height:16px} ++.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} ++.bt-news-x svg{width:12px;height:12px} ++.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} ++@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} ++.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} ++.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} ++.bt-open-new{display:flex;flex-direction:column} ++.bt-open-new .bt-new-sep{margin:18px 0 0} ++.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} ++.bt-earlier{display:flex;flex-direction:column;gap:6px} ++.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} ++.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} ++.bt-urow{display:flex;justify-content:flex-end} +.bt-urow>.bt-lead{align-self:flex-end;margin-left:6px} - .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} - .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} - .bt-ububble p{margin:0;line-height:22px} -@@ -2855,226 +2855,204 @@ function installCompiledStyles(text, tag) { - }; - } - --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); -- }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); -- }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); -- return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); -- }; -- }, [enabled]); ++.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} ++.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} ++.bt-ububble p{margin:0;line-height:22px} ++.bt-uimages{display:block;font-size:12px;opacity:.7} ++.bt-earlier .bt-event{margin:4px 0} ++.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} ++.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} ++`; ++function installStyles(text, tag) { ++ const style = document.createElement("style"); ++ style.dataset.dshBot = tag; ++ style.textContent = text; ++ document.head.appendChild(style); ++ return style; ++} ++var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; ++var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; ++function sheetClassNames(sheet) { ++ const names = []; ++ const visit = (rules) => { ++ for (const rule of rules) { ++ if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); ++ if (rule.cssRules) visit(rule.cssRules); ++ } ++ }; ++ try { ++ visit(sheet.cssRules); ++ } catch { ++ } ++ return names; ++} ++function compileFragmentSelectors(text, names) { ++ const compiled = /* @__PURE__ */ new Map(); ++ return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { ++ if (!compiled.has(fragment)) { ++ const exact = []; ++ for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); ++ compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); ++ } ++ return compiled.get(fragment); ++ }); ++} ++function installCompiledStyles(text, tag) { ++ const style = installStyles("", tag); ++ const cache = /* @__PURE__ */ new WeakMap(); ++ const render = () => { ++ const names = /* @__PURE__ */ new Set(); ++ for (const sheet of document.styleSheets) { ++ if (sheet.ownerNode?.dataset?.dshBot) continue; ++ let list = cache.get(sheet); ++ if (!list) { ++ list = sheetClassNames(sheet); ++ cache.set(sheet, list); ++ } ++ for (const name of list) names.add(name); ++ } ++ const next = compileFragmentSelectors(text, names); ++ if (style.textContent !== next) style.textContent = next; ++ }; ++ render(); ++ let timer; ++ const schedule = () => { ++ clearTimeout(timer); ++ timer = setTimeout(render, 120); ++ }; ++ const onLoad = (event) => { ++ if (event.target instanceof HTMLLinkElement) schedule(); ++ }; ++ const observer = new MutationObserver(schedule); ++ observer.observe(document.head, { childList: true }); ++ document.addEventListener("load", onLoad, true); ++ return () => { ++ observer.disconnect(); ++ document.removeEventListener("load", onLoad, true); ++ clearTimeout(timer); ++ style.remove(); ++ }; ++} + +-.bt-menu{position:fixed;pointer-events:auto;z-index:60;min-width:200px;padding:6px;border-radius:12px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1),0 0 0 1px rgba(228,228,228,.04);display:flex;flex-direction:column;gap:2px;transform-origin:var(--bt-origin,top left);animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box} +-.bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box} +-.bt-menu button:hover{background:var(--bt-hover)} +-.bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px} +-.bt-menu .bt-danger{color:#c21d2e} +-.bt-menu-sep{height:.5px;flex:none;background:var(--bt-line-2);margin:4px 8px} +-.bt-menu-up{--bt-origin:bottom left;animation-name:bt-pop-lift,bt-veil-in} +-.bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards} +-.bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums} +-.bt-palette-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:70;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out} +-.bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-palette-layer[data-leaving]{pointer-events:none} +-.bt-palette-layer[data-leaving] .bt-backdrop{opacity:0;transition:opacity .16s ease-in} +-.bt-palette-layer[data-leaving] .bt-palette{animation:bt-dialog-sink .16s cubic-bezier(.4,0,1,1) forwards} +-@keyframes bt-palette-in{from{translate:0 -10px;scale:.96}} +-.bt-palette-head{display:flex;align-items:center;gap:4px;padding:14px 10px 14px 14px;border-bottom:.5px solid var(--bt-line-2)} +-.bt-palette-glyph{display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;color:var(--bt-ink-3);margin-right:4px} +-.bt-palette-head input{flex:1;min-width:0;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0} +-.bt-palette-head input::placeholder{color:var(--bt-ink-3)} +-.bt-palette-clear{width:18px;height:18px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer} +-.bt-palette-clear:hover{color:var(--bt-ink)} +-.bt-palette-clear svg{width:12px;height:12px} +-.bt-palette-list{position:relative;height:402px;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px;box-sizing:border-box} +-.bt-palette-list>.bt-thumb,.bt-drop-list>.bt-thumb{left:0;right:0;border-radius:8px;background:var(--bt-active)} +-.bt-palette-list>.bt-thumb{left:8px;right:8px} +-:is(.bt-palette-list,.bt-drop-list)[data-glide] [aria-selected=true]{background:transparent} +-:is(.bt-palette-row,.bt-palette-section,.bt-drop-list>.bt-option){position:relative;z-index:1} +-.bt-palette-section{padding:8px 8px 4px;font-size:12px;line-height:16px;color:var(--bt-ink-3);flex:none} +-.bt-palette-section:first-of-type{padding-top:0} +-.bt-palette-row{flex:none;display:flex;align-items:center;gap:8px;height:49px;padding:6px 10px 6px 8px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;box-sizing:border-box;width:100%} +-.bt-palette-row[aria-selected=true]{background:var(--bt-active)} +-.bt-palette-lead{flex:none;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center} +-.bt-palette-cmd{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} +-.bt-palette-cmd svg{width:14px;height:14px} +-.bt-palette-agent{width:24px;height:24px;border-radius:50%;background:var(--bt-hover);display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink)} +-.bt-palette-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +-.bt-palette-title{font-size:14px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-palette-sub{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-palette-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink)} +-.bt-palette-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:0 16px;text-align:center;animation:bt-veil-in .22s ease-out} +-.bt-palette-empty-icon{color:var(--bt-line-2)} +-.bt-palette-empty-icon svg{width:32px;height:32px} +-.bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} +-.bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} +-@media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} +-.bt-account-menu{width:224px} +-.bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} +-.bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} +-/* Popover keyframes move translate and scale, which compose with a transform the popover +- already has. */ +-@keyframes bt-pop-drop{from{translate:0 -4px;scale:.96}} +-@keyframes bt-pop-lift{from{translate:0 4px;scale:.96}} +-@keyframes bt-pop-out{to{opacity:0;scale:.97;filter:blur(2px)}} +-@keyframes bt-veil-in{from{opacity:0;filter:blur(6px)}} +-@keyframes bt-check-pop{from{opacity:0;scale:.4}} +-@media (prefers-reduced-motion:reduce){.bt-menu,.bt-avedit,.bt-react-strip{animation:bt-fade .1s ease-out}.bt-react-strip button,.bt-q-check,.bt-color[aria-pressed=true] span{animation:none}} +-.bt-brand:hover{background:var(--bt-hover)} +-.bt-drop-list{position:relative;display:flex;flex-direction:column;gap:2px} +-.bt-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px} +-.bt-pick{display:flex;flex-direction:column;align-items:center;gap:4px;padding:7px 2px 6px;border-radius:10px;border:1px solid transparent;background:none;color:var(--bt-ink);cursor:pointer;font:inherit;font-size:11px;line-height:14px;min-width:0} +-.bt-pick:hover{background:var(--bt-hover)} +-.bt-pick[aria-pressed=true]{border-color:var(--bt-line-2);background:var(--bt-hover)} +-.bt-pick>span:last-child:not(.bt-mark){max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-pick:hover .bt-mark[data-state=idle] .bt-eye{animation:bt-blink .5s linear 1} +-.bt-select{width:100%;height:34px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink);padding:0 8px;font:inherit;font-size:13px} +-.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:6px} +-.bt-swatch-sep{width:1px;height:22px;background:var(--bt-line-2);margin:0 2px} +-.bt-swatch-custom{position:relative;overflow:hidden;background:conic-gradient(#ff3e51,#ffaf38,#00c972,#1cc3b0,#2a92fe,#a97efe,#ff5eb1,#ff3e51);box-sizing:border-box} +-.bt-swatch-custom[data-on=true]{border-color:var(--bt-ink)} +-.bt-swatch-custom input{position:absolute;inset:-4px;width:calc(100% + 8px);height:calc(100% + 8px);opacity:0;cursor:pointer;border:0;padding:0} +-.bt-swatch-default{background:linear-gradient(135deg,var(--bt-card) 0 46%,var(--bt-line-2) 46% 54%,var(--bt-card) 54%);box-shadow:inset 0 0 0 1px var(--bt-line-2)} +-.bt-states{display:flex;flex-wrap:wrap;gap:4px} +-.bt-state{border:1px solid var(--bt-line);background:none;border-radius:999px;font:inherit;font-size:11px;line-height:16px;padding:2px 8px;color:var(--bt-ink-2);cursor:pointer} +-.bt-state:hover{color:var(--bt-ink)} +-.bt-state[aria-pressed=true]{background:var(--bt-active);border-color:var(--bt-line-2);color:var(--bt-ink)} +-.bt-theme-chip{position:relative;width:46px;height:30px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--bt-line-2);background:linear-gradient(90deg,var(--ls) 0 36%,var(--lb) 36%);overflow:hidden} +-.bt-theme-chip i{position:absolute;right:7px;bottom:7px;width:10px;height:10px;border-radius:50%;background:var(--la)} +-body[data-ds-dark-theme] .bt-theme-chip{background:linear-gradient(90deg,var(--ds) 0 36%,var(--db) 36%)} +-body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} +-.bt-tabs{flex-wrap:wrap} +-.bt-soft:disabled{opacity:.4;cursor:default} +-.bt-group-role{margin-left:6px;font-size:11px;line-height:14px;padding:1px 5px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);vertical-align:1px} +-.bt-members{display:flex;flex-direction:column;gap:1px} +-.bt-member{display:flex;align-items:center;gap:4px;min-height:40px;padding:0 4px 0 6px;border-radius:8px;animation:bt-rise .18s ease-out} +-.bt-member:hover{background:var(--bt-hover)} +-.bt-member-name{flex:1;min-width:0;display:flex;align-items:center;gap:10px;border:0;background:none;padding:6px 0;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;cursor:pointer;text-align:left} +-.bt-member-name>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-chip-admin{background:var(--bt-tag-bg);color:var(--bt-tag-ink);border-color:transparent} +-.bt-mini{flex:none;border:0;background:none;border-radius:6px;padding:2px 6px;font:inherit;font-size:12px;line-height:18px;color:var(--bt-ink-2);cursor:pointer} +-.bt-mini:hover{background:var(--bt-active);color:var(--bt-ink)} +-.bt-member .bt-mini,.bt-member .bt-x{opacity:0;transition:opacity .15s ease} +-.bt-member:hover .bt-mini,.bt-member:hover .bt-x,.bt-member .bt-mini:focus-visible,.bt-member .bt-x:focus-visible{opacity:1} +-.bt-add{color:var(--bt-ink-2);margin-top:2px} +-.bt-add:hover,.bt-addlist .bt-option:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-addlist{display:flex;flex-direction:column;margin-top:4px;padding:4px;border:1px solid var(--bt-line);border-radius:10px;background:var(--bt-card);animation:bt-rise .16s ease-out} +-.bt-seg{position:relative;display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--bt-hover)} +-.bt-motion-seg{width:216px} +-.bt-seg button{position:relative;z-index:1;flex:1;height:30px;border:0;border-radius:8px;background:none;font:inherit;font-size:13px;color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,box-shadow .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-seg button[aria-pressed=true]{background:var(--bt-card);color:var(--bt-ink);box-shadow:0 1px 3px rgba(0,0,0,.08)} +-.bt-seg button:disabled{opacity:.4;cursor:default} +-.bt-seg button:active:not(:disabled){scale:.96} +-.bt-seg>.bt-thumb{top:2px;bottom:2px;border-radius:8px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08)} +-.bt-seg[data-glide] button[aria-pressed=true]{background:transparent;box-shadow:none} +-@keyframes bt-rise{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}} +-@media (prefers-reduced-motion:reduce){.bt-member,.bt-addlist{animation:none}} +// src/shared/face-title.js +var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function isGenericFaceTitle(name) { + const n = String(name ?? "").trim(); + return !n || n === "Face-to-face room"; - } --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 formatFaceWhen(value) { + const date = value instanceof Date ? value : new Date(value); + if (!Number.isFinite(date.getTime())) return ""; @@ -3055,13 +4757,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 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 ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -3075,9 +4771,7 @@ diff --git a/client.js b/client.js + } + rows.sort((a, b) => a.sort.localeCompare(b.sort)); + return rows.slice(0, 3).map((row) => row.owner); - } --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 uniqueOwners(owners) { + const seen = /* @__PURE__ */ new Set(); + const out = []; @@ -3090,52 +4784,17 @@ diff --git a/client.js b/client.js + } + out.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return out.slice(0, 3); - } --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 joinCodeOf(code) { + return String(code ?? "").replace(/\D/g, "").slice(0, 6); - } --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; ++} +var FACE_JOIN_WINDOW_MS = 10 * 6e4; +function faceRoomCreatedAt({ createdAt, expiresAt, startedAt } = {}) { + if (createdAt && Number.isFinite(Date.parse(createdAt))) return createdAt; + const deadline = Date.parse(expiresAt); + if (Number.isFinite(deadline)) return new Date(deadline - FACE_JOIN_WINDOW_MS).toISOString(); + return startedAt || void 0; - } --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 formatFaceRoomTitle({ name, owners, members, code, at } = {}) { + const named = String(name ?? "").trim(); + if (named && !isAutoFaceTitle(named)) return named; @@ -3146,14 +4805,7 @@ diff --git a/client.js b/client.js + if (people2.length) bits.push(people2.join(", ")); + bits.push(join.length === 6 ? join : when3); + return bits.join(" · "); - } --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]]; ++} + +// src/client/brand.js +var FLYBOT_WING = "M150 238 C 96 200, 40 196, 22 214 C 8 230, 40 262, 150 300 Z"; @@ -3175,26 +4827,12 @@ diff --git a/client.js b/client.js + const room2 = roster.roomsById?.[sessionId]; + if (bot?.remote) { + return friendBotTitle(bot); - } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; ++ } + if (room2 && room2.origin === "face-to-face" && (room2.face?.status === "closed" || room2.face?.status === "left") && isAutoFaceTitle(room2.name)) return "Face-to-face · closed"; + if (room2?.origin === "face-to-face") return faceRowTitle(roomTitle(room2)); + if (room2) return roomTitle(room2); + return bot?.name || ""; - } --function RoomAvatar({ room: room2, roster, size = 36 }) { -- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); -- const people = members.length === 1 ? [members[0], null] : members; -- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); -- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); -- if (size <= 20 && people.length > 1) { -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, -- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), -- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null -- ); ++} +function installTabTitle(sessions, roster, doc = globalThis.document) { + if (!doc || !sessions || !roster) return () => { + }; @@ -3224,25 +4862,7 @@ diff --git a/client.js b/client.js + link.rel = "icon"; + link.type = "image/svg+xml"; + document.head.appendChild(link); - } -- 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)); -- }) -- ); ++ } + link.href = href; + const retitle = () => { + const next = brandTitle(document.title); @@ -3256,127 +4876,115 @@ 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"]); -@@ -3093,14 +3093,14 @@ var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 - var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); - var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); - function PinMarkIcon() { -- return (0, import_react3.createElement)( ++ ); ++} ++var 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_react5.createElement)( - "svg", - { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( ++ "svg", ++ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + (0, import_react5.createElement)( - "g", - { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) ++ "g", ++ { transform: "rotate(45 10 10)" }, + (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), + (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) - ) - ); - } -@@ -3112,21 +3112,48 @@ var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h - var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); - var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); - var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); ++ ) ++ ); ++} ++var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); ++var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); ++var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); ++var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); ++var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); ++var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); ++var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); ++var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +var LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]); - var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); - var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); - var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); ++var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); ++var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); ++var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +var ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]); +var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]); +var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]); - var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); - var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; ++var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); ++var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +function FlyBotMark({ size = 24 }) { + return (0, import_react5.createElement)( + "svg", @@ -3401,661 +5009,945 @@ diff --git a/client.js b/client.js + ); +} - // src/client/questions.js --var import_react4 = require("react"); +-/* Memory dialog: a summary of every entry, the entries behind the menu, "Ask or update" below. */ +-.bt-mem-row{display:flex;align-items:center;gap:8px;width:100%;height:36px;padding:0 8px 0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;box-sizing:border-box;transition:background-color .12s ease} +-.bt-mem-row:hover{background:var(--bt-hover)} +-.bt-mem-row>svg{flex:none;color:var(--bt-ink-2)} +-.bt-mem-row-copy{flex:1;text-align:left} +-.bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px} +-.bt-mem-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:61;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-mem-scrim{animation:bt-fade .2s ease-out} +-.bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box} +-.bt-mem-head .bt-icon-btn{width:32px;height:32px;flex:none} +-.bt-mem-head [aria-haspopup] svg{width:18px;height:18px;stroke-width:2.6} +-.bt-mem-head>.bt-icon-btn:first-child{margin-left:-10px} +-.bt-mem-head h2{margin:0;font-size:17px;line-height:24px;font-weight:600;white-space:nowrap} +-.bt-mem-status{font-size:13px;line-height:18px;color:var(--bt-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-variant-numeric:tabular-nums} +-.bt-mem-head-gap{flex:1} +-.bt-mem-menu{position:absolute;top:52px;right:50px;min-width:220px;--bt-origin:top right} +-.bt-mem-menu button:disabled{opacity:.4;cursor:default;background:none} +-.bt-mem-body{flex:1;overflow-y:auto;padding:22px 28px 24px;scrollbar-width:thin;display:flex;flex-direction:column;gap:12px} +-.bt-mem-summary{transition:opacity .3s ease} +-.bt-mem-summary[data-stale]{opacity:.55} +-.bt-mem-summary h3{margin:20px 0 6px;font-size:17px;line-height:26px;font-weight:600} +-.bt-mem-summary h3:first-child{margin-top:0} +-.bt-mem-summary p{margin:0;font-size:15px;line-height:26px} +-.bt-mem-summary>*{animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both} +-.bt-mem-summary>:nth-child(2){animation-delay:.03s} +-.bt-mem-summary>:nth-child(3){animation-delay:.06s} +-.bt-mem-summary>:nth-child(4){animation-delay:.09s} +-.bt-mem-summary>:nth-child(5){animation-delay:.12s} +-.bt-mem-summary>:nth-child(6){animation-delay:.15s} +-.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} +-@keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} +-.bt-mem-skel{display:flex;flex-direction:column;gap:24px} +-.bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} +-.bt-mem-skel-block span{display:block;height:12px;border-radius:6px;background:linear-gradient(90deg,var(--bt-hover) 25%,var(--bt-active) 50%,var(--bt-hover) 75%);background-size:200% 100%;animation:bt-mem-skel 1.4s ease-in-out infinite} +-.bt-mem-skel-block .bt-mem-skel-head{width:30%;height:16px;margin-bottom:2px} +-@keyframes bt-mem-skel{from{background-position:150% 0}to{background-position:-50% 0}} +-.bt-mem-skel-label{font-size:13px;line-height:18px;color:var(--bt-ink-3)} +-.bt-mem-empty{margin:auto;max-width:400px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;padding:24px 0;animation:bt-mem-line .45s cubic-bezier(.16,1,.3,1) both} +-.bt-mem-empty-mark{width:48px;height:48px;border-radius:50%;background:var(--bt-hover);display:flex;align-items:center;justify-content:center;color:var(--bt-ink-2);margin-bottom:4px} +-.bt-mem-empty-mark svg{width:22px;height:22px} +-.bt-mem-empty-title{font-size:15px;line-height:22px;font-weight:600} +-.bt-mem-empty-text{font-size:13px;line-height:20px;color:var(--bt-ink-2)} +-.bt-mem-failed{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;background:var(--bt-hover);font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-mem-failed>span{flex:1;min-width:0;overflow-wrap:anywhere} +-.bt-mem-entries{display:flex;flex-direction:column;gap:28px;animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .22s ease-out} +-.bt-mem-section-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px} +-.bt-mem-section-head h3{margin:0;font-size:15px;line-height:22px;font-weight:600} +-.bt-mem-section-head span{font-size:12px;line-height:16px;color:var(--bt-ink-3)} +-.bt-mem-section-head .bt-mem-size{margin-left:auto;font-variant-numeric:tabular-nums;white-space:nowrap} +-.bt-mem-topic{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin:14px 0 2px 4px;font-weight:500} +-.bt-mem-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column} +-.bt-mem-entry{display:flex;align-items:center;gap:10px;padding:9px 4px;border-bottom:.5px solid var(--bt-line);transition:opacity .2s ease} +-.bt-mem-entry[data-busy]{opacity:.4} +-.bt-mem-entry-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} +-.bt-mem-entry-text{font-size:14px;line-height:20px;overflow-wrap:anywhere} +-.bt-mem-entry-meta{font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums} +-.bt-mem-remove{flex:none;height:28px;min-width:28px;border-radius:999px;border:0;background:none;color:var(--bt-ink-3);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0 6px;font:inherit;font-size:12px;opacity:0;transition:opacity .12s ease,background-color .12s ease,color .12s ease} +-.bt-mem-entry:hover .bt-mem-remove,.bt-mem-remove:focus-visible,.bt-mem-remove[data-confirm]{opacity:1} +-.bt-mem-remove:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-mem-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135;padding:0 10px} +-@media (hover:none){.bt-mem-remove{opacity:1}} +-.bt-mem-none{font-size:13px;line-height:18px;color:var(--bt-ink-3);padding:8px 4px} +-.bt-mem-foot{flex:none;padding:8px 20px 20px;display:flex;flex-direction:column;gap:10px} +-.bt-mem-reply{border-radius:16px;background:var(--bt-hover);padding:8px 8px 12px 16px;display:flex;flex-direction:column;gap:6px;max-height:36vh;overflow-y:auto;scrollbar-width:thin;transform-origin:50% 100%;animation:bt-pop-lift ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} +-.bt-mem-reply-head{display:flex;align-items:center;gap:8px;min-height:24px} +-.bt-mem-reply-asked{flex:1;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-mem-reply .bt-icon-btn{width:24px;height:24px;flex:none} +-.bt-mem-reply-text{font-size:14px;line-height:21px;padding-right:8px;overflow-wrap:anywhere} +-.bt-mem-changes{list-style:none;margin:2px 0 0;padding:0 8px 0 0;display:flex;flex-direction:column;gap:4px} +-.bt-mem-changes li{display:flex;align-items:flex-start;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2);overflow-wrap:anywhere} +-.bt-mem-changes li svg{flex:none;margin-top:1px;color:#e02135;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} .12s backwards} +-.bt-mem-changes li[data-ok] svg{color:var(--bt-green)} +-.bt-mem-ask{display:flex;align-items:center;gap:8px;height:52px;padding:0 8px 0 22px;border-radius:999px;border:.5px solid var(--bt-line-2);background:var(--bt-card);box-shadow:0 4px 16px rgba(0,0,0,.06);box-sizing:border-box;transition:border-color .15s ease,box-shadow .15s ease} +-.bt-mem-ask:focus-within{border-color:var(--bt-ink-3);box-shadow:0 4px 20px rgba(0,0,0,.1)} +-.bt-mem-input{flex:1;min-width:0;border:0;background:none;outline:none;color:var(--bt-ink);font:inherit;font-size:15px;line-height:22px;padding:0} +-.bt-mem-input::placeholder{color:var(--bt-ink-3)} +-.bt-mem-send{flex:none;width:36px;height:36px;border-radius:50%;border:0;background:var(--bt-ink);color:var(--bt-main);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-mem-send:disabled{opacity:.22;cursor:default} +-.bt-mem-send:not(:disabled):active{scale:.92} +-.bt-mem-send>*{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-mem-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;box-sizing:border-box;animation:bt-mem-spin .7s linear infinite} +-@keyframes bt-mem-spin{to{transform:rotate(360deg)}} +-@media (max-width:640px){.bt-mem{width:100vw;height:100%;border-radius:0;border:0}.bt-mem-body{padding:18px 18px 20px}.bt-mem-foot{padding:8px 12px 12px}} +-@media (prefers-reduced-motion:reduce){.bt-mem,.bt-mem-scrim,.bt-mem-summary>*,.bt-mem-empty,.bt-mem-reply,.bt-mem-skel-block span,.bt-mem-entries,.bt-mem-send>*,.bt-mem-changes li svg{animation:none}} ++// src/client/questions.js +var import_react6 = require("react"); - var keyLetter = (index) => String.fromCharCode(65 + index); - var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); - var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; - function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); ++var keyLetter = (index) => String.fromCharCode(65 + index); ++var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); ++var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; ++function useOptionKeys(count, onPick, ownRef) { + const pick2 = (0, import_react6.useRef)(onPick); - pick2.current = onPick; -- (0, import_react4.useEffect)(() => { ++ pick2.current = onPick; + (0, import_react6.useEffect)(() => { - if (count === 0) return void 0; - const onKey = (event) => { - if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -@@ -3146,10 +3146,10 @@ function useOptionKeys(count, onPick, ownRef) { - }, [count, ownRef]); - } - 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 +3167,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - }; - useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); - const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { ++ const options = optionsOf(question); ++ const multi = question.multiSelect === true; ++ const custom = own.trim(); ++ const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; ++ const choose = (label) => { ++ if (multi) { ++ setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); ++ return; ++ } ++ onAnswer({ selected: [label] }); ++ }; ++ const submit = () => { ++ if (!canSubmit) return; ++ onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); ++ }; ++ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); ++ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + const field = ownAnswer ? (0, import_react6.createElement)("textarea", { - ref: ownRef, - rows: 1, - value: own, -@@ -3186,7 +3186,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - }) : null; - const rows = options.map((option, optionIndex) => { - const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( ++ ref: ownRef, ++ rows: 1, ++ value: own, ++ autoComplete: "off", ++ spellCheck: false, ++ className: "bt-q-input", ++ "aria-label": t("Your own answer"), ++ placeholder: t("Type your own answer"), ++ "aria-keyshortcuts": ownLetter?.toLowerCase(), ++ onChange: (event) => setOwn(event.target.value), ++ onKeyDown: (event) => { ++ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; ++ event.preventDefault(); ++ submit(); ++ } ++ }) : null; ++ const rows = options.map((option, optionIndex) => { ++ const selected = multi && picked.includes(option.label); + return (0, import_react6.createElement)( - "button", - { - key: option.label, -@@ -3196,36 +3196,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), - onClick: () => choose(option.label) - }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( ++ "button", ++ { ++ key: option.label, ++ type: "button", ++ className: "bt-q-opt", ++ "aria-pressed": multi ? selected : void 0, ++ "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), ++ onClick: () => choose(option.label) ++ }, + (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), + (0, import_react6.createElement)( - "span", - { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null ++ "span", ++ { className: "bt-q-body" }, + (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label), + option.description ? (0, import_react6.createElement)("span", { className: "bt-q-desc" }, option.description) : null - ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null ++ ), + selected && !raised ? (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) : null - ); - }); - if (raised && field) { -- rows.push((0, import_react4.createElement)( ++ ); ++ }); ++ if (raised && field) { + rows.push((0, import_react6.createElement)( - "label", - { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) ++ "label", ++ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, + ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, field) - )); - } - let foot = null; - if (raised && multi) { -- foot = (0, import_react4.createElement)( ++ )); ++ } ++ let foot = null; ++ if (raised && multi) { + foot = (0, import_react6.createElement)( - "div", - { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) ++ "div", ++ { className: "bt-q-foot" }, + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), + (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) - ); - } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); ++ ); ++ } else if ((raised || multi) && canSubmit) { + foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); - } -- return (0, import_react4.createElement)( ++ } + return (0, import_react6.createElement)( - "form", - { - className: raised ? "bt-q bt-q-raised" : "bt-q", -@@ -3235,24 +3235,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised - submit(); - } - }, -- (0, import_react4.createElement)( ++ "form", ++ { ++ className: raised ? "bt-q bt-q-raised" : "bt-q", ++ "aria-labelledby": titleId, ++ onSubmit: (event) => { ++ event.preventDefault(); ++ submit(); ++ } ++ }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-text" }, -- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), -- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, -- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null ++ "div", ++ { className: "bt-q-text" }, + (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), + question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, + raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null - ), -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) ++ ), + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon)) - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( ++ ), + rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, + field && !raised ? (0, import_react6.createElement)( - "div", - { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null ++ "div", ++ { className: "bt-q-custom" }, + (0, import_react6.createElement)("label", { className: "bt-q-field" }, field), + !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null - ) : null, - foot - ); -@@ -3262,29 +3262,29 @@ function SettledQuestion({ question, answer }) { - const prompt = question?.question ?? ""; - const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; - if (chosen.length === 0) { -- return (0, import_react4.createElement)( ++ ) : null, ++ foot ++ ); ++} ++function SettledQuestion({ question, answer }) { ++ const options = optionsOf(question); ++ const prompt = question?.question ?? ""; ++ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; ++ if (chosen.length === 0) { + return (0, import_react6.createElement)( - "div", - { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)), + (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) - ) - ); - } -- return (0, import_react4.createElement)( ++ ) ++ ); ++ } + return (0, import_react6.createElement)( - "div", - { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { ++ "div", ++ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, + prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null, + (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { - const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( ++ const optionIndex = options.findIndex((option) => option.label === label); + return (0, import_react6.createElement)( - "div", - { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) ++ "div", ++ { key: `${label}-${index}`, className: "bt-q-opt" }, + optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)), + (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) - ); - })) - ); -@@ -3292,8 +3292,8 @@ function SettledQuestion({ question, answer }) { - function QuestionCard({ matched }) { - const pending = matched; - const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); ++ ); ++ })) ++ ); ++} ++function QuestionCard({ matched }) { ++ const pending = matched; ++ const questions = pending.questions ?? []; + const [index, setIndex] = (0, import_react6.useState)(0); + const [answers, setAnswers] = (0, import_react6.useState)([]); - const question = questions[index]; - if (question === void 0) return null; - const onAnswer = (answer) => { -@@ -3305,10 +3305,10 @@ function QuestionCard({ matched }) { - } - pending.answer({ answers: next }); - }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); ++ const question = questions[index]; ++ if (question === void 0) return null; ++ const onAnswer = (answer) => { ++ const next = [...answers, { id: question.id, ...answer }]; ++ if (index + 1 < questions.length) { ++ setAnswers(next); ++ setIndex(index + 1); ++ return; ++ } ++ pending.answer({ answers: next }); ++ }; + return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); - } - function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); ++} ++function PendingQuestion({ sessionId, question, actions, inline }) { + const [answered, setAnswered] = (0, import_react6.useState)(null); - if (answered === question.id) return null; - const onAnswer = (answer) => { - setAnswered(question.id); -@@ -3318,10 +3318,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) { - setAnswered(question.id); - void actions.dismissQuestion(sessionId); - }; -- return (0, import_react4.createElement)( ++ if (answered === question.id) return null; ++ const onAnswer = (answer) => { ++ setAnswered(question.id); ++ void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); ++ }; ++ const onDismiss = () => { ++ setAnswered(question.id); ++ void actions.dismissQuestion(sessionId); ++ }; + return (0, import_react6.createElement)( - "div", - { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ++ "div", ++ { className: inline ? "bt-q-inline" : "bt-q-dock" }, + (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) - ); - } - function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -@@ -3329,16 +3329,16 @@ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { - const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; - const loading = useSession?.(historyLoading) === true; - if (question === void 0 || question.callId && (drawn || loading)) return null; -- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); ++ ); ++} ++function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const question = useRoster((value) => value.questions?.[sessionId]); ++ const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; ++ const loading = useSession?.(historyLoading) === true; ++ if (question === void 0 || question.callId && (drawn || loading)) return null; + return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); - } - function AnsweredCell({ node }) { - const data = node.data ?? {}; - const questions = Array.isArray(data.questions) ? data.questions : []; - if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; ++} ++function AnsweredCell({ node }) { ++ const data = node.data ?? {}; ++ const questions = Array.isArray(data.questions) ? data.questions : []; ++ if (questions.length === 0) { + return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null; - } - const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { ++ } ++ const answers = Array.isArray(data.answers) ? data.answers : []; + return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, { - key: question.id, - question, - answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -@@ -3346,37 +3346,37 @@ function AnsweredCell({ node }) { - } ++ key: question.id, ++ question, ++ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } ++ }))); ++} - // src/client/secret-card.js --var import_react5 = require("react"); +-.bt-thumb{position:absolute;left:0;top:0;z-index:0;pointer-events:none;transition:opacity .14s ease} +-.bt-thumb[data-hidden]{opacity:0} +-[data-morph]{overflow:hidden!important} +-.bt-veil{animation:bt-veil-in .22s ease-out} +-.bt-with-icon{display:inline-flex;align-items:center;gap:4px} +-.bt-with-icon svg{width:14px;height:14px} +-.bt-tag-admin{display:inline-flex;align-items:center;gap:4px} +-.bt-create-inline{padding:18px 16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)} +-.bt-create-inline .bt-create-card{max-width:none} +-.bt-panel-view>[data-move=in]{animation:bt-page-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-panel-view>[data-move=out]{animation:bt-page-out ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(2){animation-delay:25ms} +-.bt-panel-view>:is([data-move=in],[data-move=out]):nth-child(n+3){animation-delay:50ms} +-.bt-panel-view>.bt-drawer-body[data-move=tab]{animation:bt-veil-in .2s ease-out} +-.bt-drawer-name-input{animation:bt-veil-in .16s ease-out} +-.bt-pop-in{display:inline-flex;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-:is(.bt-soft,.bt-send,.bt-icon-btn,.bt-settings-close,.bt-mini,.bt-tab,.bt-nc-send,.bt-avedit-tab,.bt-shape,.bt-pick,.bt-swatch,.bt-palette-clear){transition:background-color .12s ease,color .12s ease,border-color .12s ease,opacity .12s ease,${spring(SPRING_TAP, "scale")}} +-:is(.bt-icon-btn,.bt-settings-close,.bt-nc-send,.bt-shape,.bt-swatch,.bt-color,.bt-x,.bt-q-x,.bt-palette-clear):active:not(:disabled){scale:.9} +-:is(.bt-soft,.bt-send,.bt-mini,.bt-tab,.bt-avedit-tab,.bt-pick,.bt-q-submit,.bt-q-btn):active:not(:disabled){scale:.96} +-@media (prefers-reduced-motion:reduce){.bt-thumb{transition:none}.bt-panel-view>[data-move],.bt-drawer-name-input,.bt-veil,.bt-pop-in,.bt-token,.bt-drop,.bt-newchat,.bt-option-hint,.bt-connector-form,.bt-settings-head>*{animation:none}} +-`; +-var CONVERSATION_CSS = ` +-.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} +-.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} +-.bt-think summary::-webkit-details-marker{display:none} +-.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} +-.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} +-.bt-event>span{padding:0 2px} +-.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease} +-.bt-event button:hover{background:var(--bt-hover)} +-.bt-event button:active{background:var(--bt-active)} +-.bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px} +-.bt-event-whole>*{flex:none} +-.bt-event-whole>span:first-child{margin-right:2px} +-/* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */ +-.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} +-[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} +-.bt-activity{display:flex;align-items:center;gap:10px;margin:8px 0 4px 2px;font-size:14px;color:var(--bt-ink-2)} +-.bt-group-replies{display:contents} +-/* A Bot chat is flat in every work-details mode, without changing the user's mode +- (it is a DSH-wide setting): step groups lose their header and stay open, and what a +- mode folds away in a finished turn stays shown. TurnProcessCell also opens that +- fold; these rules cover the first frame before it does. */ +-[data-step-process]>div:first-child{display:none!important} +-[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} +-[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} +-[class*="_header"]:has(.bt-head){border-bottom-color:transparent!important;box-shadow:none!important} +-/* The transcript runs under a floating header pill. */ +-header[class*="_header"]:has(.bt-head){position:absolute!important;top:0;left:0;right:0;z-index:8;display:flex!important;padding:10px 0 0!important;background:transparent!important;pointer-events:none;justify-content:center} +-header[class*="_header"]:has(.bt-head)::before{content:"";position:absolute;inset:0 0 auto;height:60px;background:linear-gradient(to bottom,var(--bt-main),color-mix(in srgb,var(--bt-main) 0%,transparent));pointer-events:none;z-index:-1} +-header[class*="_header"]:has(.bt-head)>[class*="_headerLeading"]{position:absolute;left:12px;top:10px;pointer-events:auto} +-header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} +-[class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)} +-/* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators), +- without its copy and branch row; a tail with nothing in it takes no room. */ +-[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]){display:none!important} +-[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))){display:none!important} +-/* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not +- the shell's new-session hero: no headline, no workspace or mode picker, and the +- composer stays docked at the bottom. */ +-[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important} +-[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} +-[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} +-[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} +-/* The right sides give back the scrollbar gutter so both edges sit 16px from the column. */ +-[class*="_scroll"]:has(> [class*="_column"]){padding:60px 11px 16px 16px!important} +-/* The shell's running status only hosts the activity row (.bt-typing). */ +-[data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} +-[data-chat-running]>span:not([role=status]){display:none!important} +-[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} +-.bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards} +-.bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards} +-.bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} +-.bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} +-.bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} +-.bt-typing>.bt-mark .bt-eyes{transition:scale .28s cubic-bezier(.4,.06,.18,1)} +-.bt-typing>.bt-mark:hover .bt-eyes{scale:1.18} +-.bt-typing-text{display:flex;align-items:center;flex:1 1 auto;min-width:0;animation:bt-typing-label .22s ease-out .2s backwards} +-.bt-typing-label{display:inline-flex;align-items:center;gap:6px;min-width:0;animation:bt-typing-label .22s ease-out backwards} +-.bt-typing-label>.bt-shimmer{font-size:14px;line-height:22px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-typing-time{flex-shrink:0;font-size:12px;line-height:22px;color:var(--bt-ink-3);white-space:nowrap} +-@keyframes bt-typing-in{from{opacity:0;transform:scale(.9)}} +-@keyframes bt-typing-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.92);filter:blur(3px)}} +-@keyframes bt-typing-label{from{opacity:0;filter:blur(5px);transform:translateY(3px)}} +-/* Shimmer: a stepped sweep clipped to the text. */ +-.bt-shimmer{color:transparent;background:linear-gradient(90deg,var(--bt-shimmer-base) 0%,var(--bt-shimmer-base) 25%,var(--bt-ink) 60%,var(--bt-shimmer-base) 75%,var(--bt-shimmer-base) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;animation:bt-shimmer 2.2s steps(60) infinite;--bt-shimmer-base:color-mix(in srgb,var(--bt-ink) 40%,transparent)} +-@keyframes bt-shimmer{from{background-position:200% 0}to{background-position:-200% 0}} +-/* New rows slide up; a reply that replaces the activity row grows out of its corner. */ +-[data-bt-enter="new"]{animation:bt-row-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-[data-bt-enter="after-collapse"]{transform-origin:0 100%;animation:bt-row-after .24s cubic-bezier(.23,1,.32,1) .38s backwards} +-@keyframes bt-row-in{from{transform:translateY(12px)}to{transform:none}} +-@keyframes bt-row-after{0%{opacity:0;transform:translateY(12px) scale(.94)}55%{opacity:1}100%{opacity:1;transform:none}} +-@media (prefers-reduced-motion:reduce){ +-.bt-typing,.bt-typing[data-exiting]{animation-name:bt-fade-in;animation-duration:.12s;transform:none} +-.bt-typing[data-exiting]{animation-name:bt-fade-out} +-.bt-typing-text,.bt-typing-label{animation-name:bt-fade-in;animation-delay:0s} +-.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)} +-[data-bt-enter="new"]{animation:none} +-[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out} ++// 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,12 +3401,12 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { - }); - } - function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( ++ className: "bt-secret-input", ++ type: MASK_BY_CSS ? "text" : "password", ++ "data-masked": MASK_BY_CSS ? "" : void 0, ++ value, ++ autoFocus, ++ autoComplete: MASK_BY_CSS ? "off" : "new-password", ++ autoCorrect: "off", ++ autoCapitalize: "off", ++ spellCheck: false, ++ "data-1p-ignore": "", ++ "data-lpignore": "true", ++ "data-bwignore": "", ++ "data-form-type": "other", ++ "aria-label": label, ++ placeholder: t("Paste or type the key"), ++ onChange: (event) => onChange(event.target.value), ++ onKeyDown: (event) => { ++ if (event.key !== "Enter" || event.nativeEvent.isComposing) return; ++ event.preventDefault(); ++ onEnter?.(); ++ } ++ }); ++} ++function 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, text]) => (0, import_react7.createElement)("button", { - key: id, - type: "button", - role: "radio", -@@ -3417,26 +3417,26 @@ function ScopeSwitch({ value, onChange, bot }) { - ); - } - function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( ++ key: id, ++ type: "button", ++ role: "radio", ++ "aria-checked": value === id, ++ className: "bt-secret-seg", ++ onClick: () => onChange(id) ++ }, text)) ++ ); ++} ++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)(); ++ ); ++} ++function SecretCard({ bot, request, roster, actions }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); + const [replacing, setReplacing] = (0, import_react7.useState)(false); + const [busy, setBusy] = (0, import_react7.useState)(false); + const [error, setError] = (0, import_react7.useState)(""); + const noteId = (0, import_react7.useId)(); - const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); - const run = (promise) => { - setBusy(true); -@@ -3452,73 +3452,73 @@ function SecretCard({ bot, request, roster, actions }) { - if (!busy) void run(actions.secretAllow(request.id)); - }; - const 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 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_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": text, "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" }, ++ text, + 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 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 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; - } -@@ -3531,44 +3531,46 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { - const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); - const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); - if (waiting.length === 0) return null; -- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); ++ } ++ return null; ++} ++function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { ++ const roster = useRoster((value) => value); ++ const shown = useCards?.((state) => state[sessionId]) ?? NONE; ++ const loading = useSession?.(historyLoading) === true; ++ const bot = roster.byId[sessionId]; ++ if (!bot || currentPart(bot) !== sessionId) return null; ++ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); ++ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); ++ if (waiting.length === 0) return null; + return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); - } ++} +var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; - function ScopeEditor({ secret, roster, onSave }) { -- const [mode, setMode] = (0, import_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, text]) => (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) }), ++ "label", ++ { key: id, className: "bt-secret-radio" }, + (0, import_react7.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", { ++ text ++ )) ++ ), + 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); +-@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} +-@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} +-[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +-[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} +-[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} +-[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} +-[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} +-[data-composer-card]>[class*="_row"]{display:contents!important} +-[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} +-[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} +-[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} +-[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} +-[data-composer-placeholder]{font-size:0!important;line-height:0!important} +-[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} +-/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ +-[data-composer-card] button[class*="_add"]{display:none!important} +-.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-plus:active{scale:.9} +-.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} +-.bt-plus[aria-expanded=true] svg{rotate:45deg} +-.bt-plus-menu{min-width:180px} +-[data-composer-card] [class*="_tools"]{gap:0!important} +-[data-composer-card] [class*="_modes"]:empty{display:none!important} +-[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} +-[data-composer-card] [class*="_trailing"]{gap:8px} +-[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} +-[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} +-[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} +-@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} +-[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} +-[data-composer-card] button[class*="_primary"]:disabled{display:none!important} +-[data-composer-dock]{display:none!important} +-.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} +-.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} +-@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} +-.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-voice:hover{opacity:.9} +-.bt-mic:active,.bt-voice:active{scale:.9} +-[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} +-body[data-bt-room] .bt-voice{display:none!important} +-body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} +-.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} +-[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} +-.bt-q{order:1} +-div:has(> nav [class*="_marks"]){display:none!important} +-/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) +- takes its surface and its text from the one --bt-user pair: a skin that repaints +- the host's bubble surface alone would otherwise leave the text unreadable. */ +-[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} +-[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} +-/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ +-[class*="_userRow"] [class*="_actions"]{display:none!important} +-[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} +-[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} +-[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} +-[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} +-[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} +-[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} +-[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} +-.bt-q-inline{display:flex;margin:4px 0 6px} +-[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} +-[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} +-.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} +-.bt-q-dock{width:100%;display:flex;justify-content:center} +-.bt-q-dock .bt-q{margin:0 auto 8px} +-[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} +-[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} +-[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} +-[class*="_toBottomSlot"]>button:active{scale:.9} +-[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} +-[class*="_toBottomSlot"]>button svg{display:none} +-[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} +-@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} +-[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} +-.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-@keyframes bt-news-in{from{translate:0 8px;scale:.9}} +-.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} +-@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} +-.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} +-.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} +-.bt-news-ico svg{width:16px;height:16px} +-.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} +-.bt-news-x svg{width:12px;height:12px} +-.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} +-@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} +-.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} +-.bt-open-new{display:flex;flex-direction:column} +-.bt-open-new .bt-new-sep{margin:18px 0 0} +-.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} +-.bt-earlier{display:flex;flex-direction:column;gap:6px} +-.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} +-.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} +-.bt-urow{display:flex;justify-content:flex-end} +-.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} +-.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} +-.bt-ububble p{margin:0;line-height:22px} +-.bt-uimages{display:block;font-size:12px;opacity:.7} +-.bt-earlier .bt-event{margin:4px 0} +-.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} +-.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} ++function ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); - const long = value.trim().length >= MIN_VALUE; - const save = () => { - if (!long) return; -@@ -3583,66 +3583,66 @@ function ValueEditor({ secret, onSave }) { - setValue(next); - setReplaced(false); - }; -- return (0, import_react5.createElement)( ++ const long = value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (!long) return; ++ void Promise.resolve(onSave(value)).then((ok) => { ++ if (ok) { ++ setValue(""); ++ setReplaced(true); ++ } ++ }); ++ }; ++ const 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)(() => { ++ ); ++} ++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 - ); - } - 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)(""); ++ ) ++ ) : null ++ ); ++} ++function AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); + const [value, setValue] = (0, import_react7.useState)(""); - const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; - const save = () => { - if (!ready) return; -@@ -3653,43 +3653,43 @@ function AddSecret({ actions, run, onDone }) { - } - }); - }; -- return (0, import_react5.createElement)( ++ const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; ++ const save = () => { ++ if (!ready) return; ++ void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { ++ if (ok) { ++ setValue(""); ++ onDone(); ++ } ++ }); ++ }; + return (0, import_react7.createElement)( - "div", - { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-set-row bt-set-paste bt-secret-add" }, + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), + (0, import_react7.createElement)( - "div", - { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) ++ "div", ++ { className: "bt-secret-row-actions" }, + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) - ) - ); - } - function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); ++ ) ++ ); ++} ++function SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); - const secrets = roster.secrets ?? []; - return [ -- (0, import_react5.createElement)( ++ const secrets = roster.secrets ?? []; ++ return [ + (0, import_react7.createElement)( - "p", - { key: "about", className: "bt-secret-about" }, - t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), ++ "p", ++ { key: "about", className: "bt-secret-about" }, ++ t("Keys your Bots asked for. Bots never see the values: they use them as "), + (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"), - t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), ++ t(" in their shell, and every value is replaced by "), + (0, import_react7.createElement)("code", null, "[secret:NAME]"), - t(" before anything goes to a model.") - ), -- (0, import_react5.createElement)( ++ t(" before anything goes to a model.") ++ ), + (0, import_react7.createElement)( - Section, - { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null ++ Section, ++ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, + secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, + secrets.map((secret) => (0, import_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), + adding ? (0, import_react7.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null - ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) ++ ), + adding ? null : (0, import_react7.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) - ]; - } - var SECRET_CSS = ` -@@ -3756,6 +3756,144 @@ var SECRET_CSS = ` - @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} ++ ]; ++} ++var SECRET_CSS = ` ++.bt-secret-head{align-items:center;gap:10px} ++.bt-secret-who{position:relative;flex:none;display:inline-flex} ++.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} ++.bt-secret-lock svg{width:11px;height:11px} ++.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} ++.bt-secret-body{display:flex;flex-direction:column;gap:8px} ++.bt-secret-field{align-items:center;min-height:36px} ++.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} ++.bt-secret-input[data-masked]{-webkit-text-security:disc} ++.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} ++.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} ++.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} ++.bt-secret-glide[data-at="1"]{transform:translateX(100%)} ++.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} ++.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} ++.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} ++/* A key card asks the user to act, so the process group carrying it stays open in every ++ work-details mode (same override as .bt-group-replies in styles.js). */ ++[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} ++[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} ++.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} ++.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} ++@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} ++.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} ++.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} ++.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} ++.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} ++.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} ++.bt-secret-done .bt-q-head{align-items:center} ++.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} ++.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} ++.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} ++.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} ++@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} ++@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} ++.bt-secret-event{display:inline-flex;align-items:center;gap:5px} ++.bt-secret-dock{flex-direction:column;align-items:center} ++.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} ++.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} ++.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} ++.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} ++.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} ++.bt-secret-faces{display:inline-flex} ++.bt-secret-faces>*+*{margin-left:-6px} ++.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} ++.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} ++.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} ++.bt-secret-radios{display:flex;gap:16px;font-size:13px} ++.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} ++.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} ++.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} ++.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} ++.bt-secret-bot svg{width:13px;height:13px} ++.bt-secret-inline{display:flex;gap:8px;align-items:center} ++.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} ++.bt-secret-row-actions .bt-set-hint{flex:1} ++.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} ++.bt-secret-danger svg{width:13px;height:13px} ++.bt-secret-add .bt-input{height:34px} ++@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} `; - +-function installStyles(text, tag) { +- const style = document.createElement("style"); +- style.dataset.dshBot = tag; +- style.textContent = text; +- document.head.appendChild(style); +- return style; ++ +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -4194,61 +6086,65 @@ diff --git a/client.js b/client.js + ); +} + - // src/client/turn-reasoning.js - function turnReasoning(steps, current) { - const members = steps.length ? steps : [current]; -@@ -3767,7 +3767,7 @@ var thinkingLive = (step) => step.status === "running" && texts(step).length === - var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); - - // src/client/cells.js --var import_react6 = require("react"); ++// 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,7 +3780,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, - actions.openSession(target); - }; - const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( ++var import_react_dom = require("react-dom"); ++var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); ++var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); ++function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { ++ const onClick = (event) => { ++ const target = mentionTarget(event); ++ if (target === null) return; ++ event.preventDefault(); ++ event.stopPropagation(); ++ actions.openSession(target); ++ }; ++ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; + return (0, import_react9.createElement)( - "div", - { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, - texts2.map((text, index) => { -@@ -3788,21 +3788,21 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, - const picked = key ? reactions?.[index] ?? [] : []; - const last = index === texts2.length - 1; - const live = streaming && last; -- return (0, import_react6.createElement)( ++ "div", ++ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, ++ texts2.map((text, index) => { ++ const key = msgKey ? `${msgKey}:${index}` : void 0; ++ const picked = key ? reactions?.[index] ?? [] : []; ++ const last = index === texts2.length - 1; ++ const live = streaming && last; + return (0, import_react9.createElement)( - "div", - { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( -+ (0, import_react9.createElement)( - "div", - { className: "bt-msg-line" }, - lead && last ? lead : null, -- (0, import_react6.createElement)( ++ "div", ++ { key: index, className: "bt-msg" }, ++ (0, import_react9.createElement)( ++ "div", ++ { className: "bt-msg-line" }, ++ lead && last ? lead : null, + (0, import_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(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) - ), -- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) ++ ), + live ? null : (0, import_react9.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", { ++ ), + picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { - key: emoji, - type: "button", - className: "bt-react", -@@ -3813,9 +3813,22 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, - }) - ); - } ++ key: emoji, ++ type: "button", ++ className: "bt-react", ++ "aria-label": t("Remove reaction {emoji}", { emoji }), ++ onClick: () => actions.toggleReaction(msgKey, index, emoji) ++ }, emoji))) : null ++ ); ++ }) ++ ); ++} +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; - var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; ++var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text) { + if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; + const match = String(text ?? "").trim().match(/^(.+) left(?: the room)?$/); @@ -4261,90 +6157,102 @@ 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; -@@ -3836,11 +3836,11 @@ function useDismiss(open, close, refs) { - }, [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, 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) => { -@@ -3865,25 +3865,25 @@ function MessageTools({ text, name, sessionId, react, actions }) { - link.click(); - setTimeout(() => URL.revokeObjectURL(url), 1e3); - }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { ++ const close = () => setPanel(null); ++ useDismiss(panel !== null, close, [tools, pop]); ++ const open = (kind, event) => { ++ if (panel === kind) { ++ close(); ++ return; ++ } ++ const rect = event.currentTarget.getBoundingClientRect(); ++ const below = rect.bottom + 220 < window.innerHeight; ++ setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); ++ setPanel(kind); ++ }; ++ const copy2 = () => { ++ void navigator.clipboard?.writeText(text).then(() => { ++ setCopied(true); ++ setTimeout(() => setCopied(false), 1e3); ++ }); ++ }; ++ const download2 = () => { ++ const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); ++ const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); ++ link.click(); ++ setTimeout(() => URL.revokeObjectURL(url), 1e3); ++ }; + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { - close(); - run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( ++ close(); ++ run(); + } }, (0, import_react9.createElement)(Icon), label); + return (0, import_react9.createElement)( - "div", - { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( ++ "div", ++ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (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), -@@ -3892,11 +3892,12 @@ function MessageTools({ text, name, sessionId, react, actions }) { - ); - } - function ThinkingRow({ text }) { -- return (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 }) { + if (typeof text !== "string" || text.trim() === "") return null; + return (0, import_react9.createElement)( - "details", - { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) ++ "details", ++ { className: "bt-think" }, + (0, import_react9.createElement)("summary", null, (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), t("Thought it through")), + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text) - ); - } - function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,19 +3904,33 @@ function useFileMentions({ node, openFile, fileMentions, useTurnData }) { - const location2 = node.location; - const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; - const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { ++ ); ++} ++function 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; @@ -4356,45 +6264,41 @@ diff --git a/client.js b/client.js + if (texts2.every((text) => /^Not sent:/.test(text.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((text) => text.trim() !== ""); - const running = data.status === "running"; - useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); - if (hidden) return null; -@@ -3924,13 +3924,14 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use - const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; - if (texts2.length === 0 && !reasoning && relays.length === 0) return null; - const selfId = roster.byId[sessionId]?.id ?? sessionId; -- return (0, import_react6.createElement)( -- import_react6.Fragment, ++ const running = data.status === "running"; ++ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); ++ if (hidden) return null; ++ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); ++ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; ++ if (texts2.length === 0 && !reasoning && relays.length === 0) return null; ++ const selfId = roster.byId[sessionId]?.id ?? sessionId; + const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null; + return (0, import_react9.createElement)( + import_react9.Fragment, - null, - relays.map((relay) => { - const source = relay.source; - const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { ++ null, ++ relays.map((relay) => { ++ const source = relay.source; ++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + return (0, import_react9.createElement)(EventLine, { - key: relay.seq, - prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), - bots: [sender], -@@ -3940,61 +3940,230 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use - whole: true - }); - }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null ++ key: relay.seq, ++ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), ++ bots: [sender], ++ onOpen: () => { ++ if (sender.id) actions.openExchange(selfId, sender.id); ++ }, ++ whole: true ++ }); ++ }), + reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles + ); @@ -4417,47 +6321,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 = (text) => String(text ?? "").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, text, bot) { @@ -4470,7 +6362,20 @@ diff --git a/client.js b/client.js + const held = heldForm(item.text); + return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held)); + }); -+} + } +-var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; +-var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; +-function sheetClassNames(sheet) { +- const names = []; +- const visit = (rules) => { +- for (const rule of rules) { +- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); +- if (rule.cssRules) visit(rule.cssRules); +- } +- }; +- try { +- visit(sheet.cssRules); +- } catch { +var HELD_SENDING_MS = 6e4; +function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) { + if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0; @@ -4491,15 +6396,14 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) + ); -+ } + } +- return names; + if (held === "held" || isHeldLine(room2, text, 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)) } }, @@ -4507,13 +6411,21 @@ diff --git a/client.js b/client.js + room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + ); + return (0, import_react9.createElement)( - "div", - { className: "bt-group-msg" }, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-group-msg" }, + name, + (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + ); -+} + } +-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 remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -4526,7 +6438,7 @@ diff --git a/client.js b/client.js + ownerName: member?.homeName || member?.owner || chat?.remote?.homeName || chat?.remote?.owner, + handle, + isChief: member?.isChief ?? chat?.remote?.isChief -+ }); + }); + const bot = seat ? lookBotForRemote({ handle, displayName: name, ...seat }, roster) : chat ?? lookBotForRemote({ + handle, + displayName: name, @@ -4574,18 +6486,28 @@ 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 }) ++ ), + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null + ); -+} + } +-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 FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; @@ -4618,8 +6540,28 @@ diff --git a/client.js b/client.js + setError(String(reason?.message ?? reason)); + } finally { + setBusy(""); -+ } -+ }; + } +- 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(); + }; + return (0, import_react9.createElement)( + "div", + { className: "bt-file-chips" }, @@ -4629,231 +6571,346 @@ diff --git a/client.js b/client.js + `📎 ${file.name} · ${fileSize(file.size)}` + )), + error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null - ); ++ ); } - function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( +- +-// src/client/mark.js +-var import_react2 = require("react"); +-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +-function useGaze(ref, enabled) { +- (0, import_react2.useEffect)(() => { +- if (!enabled) return void 0; +- let frame = 0; +- let point = null; +- const place = () => { +- frame = 0; +- const node = ref.current; +- if (!node || !point) return; +- const box = node.getBoundingClientRect(); +- const clamp = (value) => Math.max(-1, Math.min(1, value)); +- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); +- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); +- }; +- const onMove = (event) => { +- point = { x: event.clientX, y: event.clientY }; +- if (!frame) frame = requestAnimationFrame(place); +- }; +- const onLeave = () => { +- point = null; +- ref.current?.style.setProperty("--bt-gx", "0"); +- ref.current?.style.setProperty("--bt-gy", "0"); +- }; +- window.addEventListener("pointermove", onMove, { passive: true }); +- document.documentElement.addEventListener("pointerleave", onLeave); +- return () => { +- cancelAnimationFrame(frame); +- window.removeEventListener("pointermove", onMove); +- document.documentElement.removeEventListener("pointerleave", onLeave); +- }; +- }, [enabled]); ++function 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 ++ "div", ++ { className: "bt-event" }, + (0, import_react9.createElement)("span", null, prefix), + room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null - ); ++ ); } - function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -@@ -4005,14 +4005,14 @@ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { - const target = other && other.id !== self?.id ? other : self; - if (!target) return null; - const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); -- return (0, import_react6.createElement)( +-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 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); -+ return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null); ++ ); } - var scrollParent = (node) => { - for (let at = node.parentElement; at; at = at.parentElement) { -@@ -4021,11 +4021,11 @@ var scrollParent = (node) => { - return null; - }; - function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); +-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 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) { ++ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; ++ } ++ return null; ++}; ++function EarlierParts({ sessionId, roster, actions }) { + const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react9.useRef)(null); + const busy = (0, import_react9.useRef)(false); + const alive = (0, import_react9.useRef)(true); + const restore = (0, import_react9.useRef)(null); - const load = (cursor) => { - if (busy.current) return; - busy.current = true; -@@ -4041,19 +4041,19 @@ function EarlierParts({ sessionId, roster, actions }) { - if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); - }); - }; -- (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 +4069,35 @@ function EarlierParts({ sessionId, roster, actions }) { - if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); - else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); - } -- return (0, import_react6.createElement)( ++ const node = top.current; ++ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; ++ const observer = new IntersectionObserver((entries2) => { ++ if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); ++ }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); ++ observer.observe(node); ++ return () => observer.disconnect(); ++ }, [view.cursor, view.loading, view.failed]); ++ const selfId = roster.byId[sessionId]?.id; ++ const rows = []; ++ for (const item of view.items) { ++ const last = rows.at(-1); ++ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); ++ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); ++ } + return (0, import_react9.createElement)( - "div", - { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( ++ "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 ++ "div", ++ { 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 +4105,7 @@ function EarlierParts({ sessionId, roster, actions }) { - return null; - } - }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) ++ if (selfId && other.id) actions.openExchange(selfId, other.id); ++ }, whole: true }); ++ } ++ default: ++ return null; ++ } ++ }), + (0, import_react9.createElement)(PartLine, { time: view.startedAt }) ); } - function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -@@ -4116,7 +4116,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - const deferred = source.kind === "bot" && source.deferred === true; - const relayed = isRelayContext(data); - const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { +-function 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 TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { ++ const roster = useRoster((value) => value); ++ const data = node.data; ++ const source = data.source !== null && typeof data.source === "object" ? data.source : {}; ++ const selfId = roster.byId[sessionId]?.id ?? sessionId; ++ const deferred = source.kind === "bot" && source.deferred === true; ++ const relayed = isRelayContext(data); ++ const turn = node.location?.turn?.turn ?? data.turn; + (0, import_react9.useLayoutEffect)(() => { - if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); - }, [relayed, sessionId, turn, source.senderSessionId]); - const order = useSessions((list) => { -@@ -4135,23 +4135,31 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; - }); - const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; ++ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); ++ }, [relayed, sessionId, turn, source.senderSessionId]); ++ const order = useSessions((list) => { ++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; ++ return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; ++ }); ++ const peers = useTurnClock((state) => { ++ if (!relayed) return ""; ++ const turns = state[sessionId] ?? {}; ++ return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const hiddenTurns = roster.exchangeTurns[sessionId]; ++ const unreadOpening = useTurnClock((state) => { ++ const first = openingTurn(state[sessionId], order, hiddenTurns); ++ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++ }); ++ const body = triggerBody(); + return unreadOpening ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; - function triggerBody() { ++ function triggerBody() { + if (source.form === "remote-chat") return null; + if (source.shown === true) return null; + if (source.remote?.handle) { + const message = (0, import_react9.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)(FirstContact, { source, roster, sessionId }), message) : message; + } - if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; ++ if (source.kind === "bot") { + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions }); + if (source.role === "room" || source.role === "kickoff") return null; + if (source.role === "mention") return (0, import_react9.createElement)("div", { className: "bt-event" }, (0, import_react9.createElement)("span", null, contentText(data.content))); - if (source.role === "secret" || deferred) return null; - if (source.role === "post") { - if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; ++ if (source.role === "secret" || deferred) return null; ++ if (source.role === "post") { ++ if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.notice === true || source.senderSessionId === "face-notice") return null; - const room2 = roster.roomsById[sessionId]; - const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); ++ const room2 = roster.roomsById[sessionId]; ++ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); - } - if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); ++ } ++ if (source.role === "report") { + return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); - } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); ++ } + if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); + if (source.role === "retry" || source.retried === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); - const run = relayed ? commRunOf( - order.split(",").filter(Boolean).map(Number), - Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -@@ -4165,7 +4165,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { - if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); - } -- return (0, import_react6.createElement)(EventLine, { ++ const run = relayed ? commRunOf( ++ order.split(",").filter(Boolean).map(Number), ++ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), ++ turn, ++ hiddenTurns ++ ) : null; ++ if (run === "folded") return null; ++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; ++ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); ++ const bots = [sender]; ++ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { ++ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); ++ } + return (0, import_react9.createElement)(EventLine, { - prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), - bots, - onOpen: (bot) => actions.openExchange(selfId, bot.id), -@@ -4174,24 +4174,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us - } - if (source.kind === "schedule") { - const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); ++ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), ++ bots, ++ onOpen: (bot) => actions.openExchange(selfId, bot.id), ++ whole: true ++ }); ++ } ++ if (source.kind === "schedule") { ++ const prompts = reminderPrompts(contentText(data.content)); + return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); - } - if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); ++ } ++ if (source.kind === "user-question-reply") return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); - } ++ } } - function NewDivider() { -- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); +-function 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 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 useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; +- } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { ++ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + (0, import_react9.useEffect)(() => { - if (!open) turnProcess.setOpen(true); - }, [open, turnProcess]); - const turn = node?.location?.turn?.turn ?? node?.data?.turn; - const start = node?.location?.turn?.start?.time; - const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { ++ if (!open) turnProcess.setOpen(true); ++ }, [open, turnProcess]); ++ const turn = node?.location?.turn?.turn ?? node?.data?.turn; ++ const start = node?.location?.turn?.start?.time; ++ const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); + (0, import_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "first", start); - }, [sessionId, turn, start]); - const visit = useUi((state) => state.visits?.[sessionId]); -@@ -4199,11 +4199,63 @@ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTu - const own = state[sessionId]?.[turn]; - return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; - }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; ++ actions.noteTurnTime(sessionId, turn, "first", start); ++ }, [sessionId, turn, start]); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const fresh = useTurnClock((state) => { ++ const own = state[sessionId]?.[turn]; ++ return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; ++ }); + return fresh ? (0, import_react9.createElement)(NewDivider) : null; } - function Nothing() { - return null; +-function 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 Nothing() { ++ return 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]]; +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([ @@ -4902,17 +6959,78 @@ diff --git a/client.js b/client.js + if (isHiddenNotice(block)) continue; + if (block?.type === "tool-removal" && block.toolName) parts.push(`Tool removed: ${block.toolName}`); + else if (block?.type === "tool-addition" && block.toolName) parts.push(`Tool added: ${block.toolName}`); -+ } + } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; + if (parts.length === 0) return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · ")); -+} - var SEPARATOR_GAP_MS = 30 * 60 * 1e3; - function clockLabel(time, now = Date.now()) { - const date = new Date(time); -@@ -4233,10 +4233,19 @@ function openingTurn(turns, firstTurns, hiddenTurns) { - return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; } - var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +-function RoomAvatar({ room: room2, roster, size = 36 }) { +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const people = members.length === 1 ? [members[0], null] : members; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); ++var SEPARATOR_GAP_MS = 30 * 60 * 1e3; ++function clockLabel(time, now = Date.now()) { ++ const date = new Date(time); ++ const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); ++ const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); ++ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); ++ if (days === 0) return t("Today {time}", { time: clock4 }); ++ if (days === 1) return t("Yesterday {time}", { time: clock4 }); ++ const sameYear = date.getFullYear() === new Date(now).getFullYear(); ++ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; ++} ++function nextTurnStart(turns, turn) { ++ let next; ++ for (const key in turns) { ++ const candidate = Number(key); ++ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; + } +- 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)); +- }) +- ); ++ return next === void 0 ? void 0 : turns[next].first; + } +- +-// 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 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 SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); ++var 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 ?? {}) { @@ -4920,107 +7038,202 @@ diff --git a/client.js b/client.js + if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true; + } + return false; -+} + } +-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" }) +var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; - function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { - const turn = location2.turn; - const time = location2.end.time; -- (0, import_react6.useEffect)(() => { ++function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { ++ const turn = location2.turn; ++ const time = location2.end.time; + (0, import_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "end", time); - }, [sessionId, turn, time]); - const label = useTurnClock((state) => { -@@ -4250,15 +4250,15 @@ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions - return start !== void 0 && start > visit.seen && start <= visit.entered; - }); - if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, ++ actions.noteTurnTime(sessionId, turn, "end", time); ++ }, [sessionId, turn, time]); ++ const label = useTurnClock((state) => { ++ const start = nextTurnStart(state[sessionId] ?? {}, turn); ++ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const fresh = useTurnClock((state) => { ++ if (typeof visit?.seen !== "number" || time > visit.seen) return false; ++ const start = nextTurnStart(state[sessionId] ?? {}, turn); ++ return start !== void 0 && start > visit.seen && start <= visit.entered; ++ }); ++ if (!label && !fresh) return null; + return (0, import_react9.createElement)( + import_react9.Fragment, - null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++ null, + label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + fresh ? (0, import_react9.createElement)(NewDivider) : null ); } - function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +-function 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 Shimmer({ children }) { + return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children); } - var PHRASES = { - thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -@@ -4320,19 +4320,22 @@ function toolActivity(name, arg, findBot, self) { +-function MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), +- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) +- ); ++var 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("On it"); +-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 workPhrase(tool) { ++ if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); -+} + } +-function ShareIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +- ); +function 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 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 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]); ++ 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)(() => { +-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); +-var lineIcon = (paths) => function LineIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) +- ); +-}; +-var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; +-function CloudDownloadIcon({ done = false }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: CLOUD }), +- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) +function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) { + const [live, setLive] = (0, import_react9.useState)({ replies: [] }); + (0, import_react9.useEffect)(() => { - if (done) return void 0; - let stop = false; - let timer; -@@ -4341,10 +4341,10 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi - const result = await actions.roomProgress(sessionId); - if (stop) return; - const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; ++ if (done) return void 0; ++ let stop = false; ++ let timer; ++ let seen = ""; ++ const tick = async () => { ++ const result = await actions.roomProgress(sessionId); ++ if (stop) return; ++ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; + const key = entry ? JSON.stringify(entry.replies) : ""; - if (entry && key !== seen) { - seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); ++ if (entry && key !== seen) { ++ seen = key; + setLive({ replies: entry.replies ?? [] }); - } - timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); - }; -@@ -4362,71 +4362,89 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi - replies = []; - } - } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; ++ } ++ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); ++ }; ++ void tick(); ++ return () => { ++ stop = true; ++ clearTimeout(timer); ++ }; ++ }, [done, sessionId, callId]); ++ let replies = live.replies; ++ if (done) { ++ try { ++ replies = JSON.parse(contentText(content)).replies ?? []; ++ } catch { ++ replies = []; ++ } ++ } + if (replies.length === 0) return null; - const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ const room2 = roster.roomsById[sessionId]; + return (0, import_react9.createElement)( - "div", - { className: "bt-group-replies" }, - replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; ++ "div", ++ { className: "bt-group-replies" }, ++ replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; + const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored; + const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; - if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( ++ if (reply.kind === "user") { + const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId; + if (reply.other) { + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( - "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); ++ "div", + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( @@ -5030,67 +7243,45 @@ diff --git a/client.js b/client.js + ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); - } - if (reply.kind === "failed") { -- 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") { @@ -5110,154 +7301,344 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react9.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); - }) ++ }) ); } -@@ -4435,52 +4435,52 @@ var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); - function callTree(root) { - const calls = []; - const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; +-var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); +-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); +-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); +-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); +-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); +-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); +-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); +-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); +-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); +-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); +-var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); +-var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); +-function PinMarkIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( +- "g", +- { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) +- ) +- ); ++var 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; ++ }; ++ visit(root); ++ return calls; } - function useShownCards(actions, sessionId, callIds) { - const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { +-var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); +-var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); +-var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); +-var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); +-var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); +-var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); +-var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); +-var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +-var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); +-var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); +-var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +-var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); +-var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +- +-// src/client/questions.js +-var import_react4 = require("react"); +-var keyLetter = (index) => String.fromCharCode(65 + index); +-var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); +-var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; +-function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); +- pick2.current = onPick; +- (0, import_react4.useEffect)(() => { +- if (count === 0) return void 0; +- const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); +- } +- }; +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); ++function 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]); ++ const ids = JSON.parse(key); ++ if (ids.length === 0 || !actions.noteCards) return void 0; ++ actions.noteCards(sessionId, ids, 1); ++ return () => actions.noteCards(sessionId, ids, -1); ++ }, [sessionId, key]); } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); +-function 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; +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 }); +- onAnswer({ selected: [label] }); +- }; +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); +- }; +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); ++ case "stop_bot": { ++ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); - } - case "create_bot": { - const created = findBot(arg("name")); - if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); ++ } ++ case "create_bot": { ++ const created = findBot(arg("name")); ++ if (!done || created === void 0) return null; + return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); - } - case "update_bot": { - const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; ++ } ++ case "update_bot": { ++ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); + return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; - } - case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); ++ } ++ case "group_relay": + return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi }); - case "create_group": - case "update_group": - case "delete_group": -@@ -4492,64 +4492,80 @@ function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot - post_to_group: ["Posted in", "Couldn't post in the group"] - }[tool]; - if (!done) return null; -- const result = contentText(call2.content); ++ case "create_group": ++ case "update_group": ++ case "delete_group": ++ case "post_to_group": { ++ const verbs = { ++ create_group: ["Created group", "Couldn't create the group"], ++ update_group: ["Updated group", "Couldn't update the group"], ++ delete_group: ["Deleted group", "Couldn't delete the group"], ++ post_to_group: ["Posted in", "Couldn't post in the group"] ++ }[tool]; ++ if (!done) return null; + const result = contentText(call4.content); - const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); ++ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); + if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); - const name = named[2]; - const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); ++ const name = named[2]; ++ const room2 = roster.rooms.find((entry) => entry.name === name); + return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); - } - case "ask_user": { - const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); ++ } ++ case "ask_user": { ++ const question = roster.questions?.[sessionId]; + if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true }); + return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); - } - case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); ++ } ++ case "request_secret": + return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions }); - case "ask_user_question": { - if (!done) return null; - let parsed = null; - try { -- parsed = JSON.parse(contentText(call2.content)); ++ case "ask_user_question": { ++ if (!done) return null; ++ let parsed = null; ++ try { + parsed = JSON.parse(contentText(call4.content)); - } catch { - parsed = null; - } - 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; +- }) : null; +- const rows = options.map((option, optionIndex) => { +- const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( +- "button", +- { +- key: option.label, +- type: "button", +- className: "bt-q-opt", +- "aria-pressed": multi ? selected : void 0, +- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), +- onClick: () => choose(option.label) +- }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( +- "span", +- { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null +- ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( +- "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); ++ case "read_group_chat": { ++ if (!done) return null; ++ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); + return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; - } - case "remember": - case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; ++ } ++ case "remember": ++ case "forget": + return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null; - default: - return null; ++ default: ++ return null; } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) +- ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( +- "div", +- { className: "bt-q-custom" }, +- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), +- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null +- ) : null, +- foot +- ); } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +-function SettledQuestion({ question, answer }) { +- const options = optionsOf(question); +- const prompt = question?.question ?? ""; +- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; +- if (chosen.length === 0) { +- return (0, import_react4.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) +- ) +- ); +- } +- return (0, import_react4.createElement)( +- "div", +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) +- ); +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { - const roster = useRoster((value) => value); - const root = node.data.root; - const turn = node.location?.turn?.turn ?? root.turn; - const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); ++ const roster = useRoster((value) => value); ++ const root = node.data.root; ++ const turn = node.location?.turn?.turn ?? root.turn; ++ const hidden = isHiddenTurn(roster, sessionId, turn); + const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId); - const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); ++ const calls = hidden ? [] : callTree(root); + const drawn = calls.filter((call4) => DRAWN.has(toolName(call4))); + const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result"); - const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; - useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); ++ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; ++ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); + useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId)); - if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); ++ if (drawn.length === 0) return null; + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); -+} + } +-function QuestionCard({ matched }) { +- const pending = matched; +- const questions = pending.questions ?? []; +- const [index, setIndex] = (0, import_react4.useState)(0); +- const [answers, setAnswers] = (0, import_react4.useState)([]); +- const question = questions[index]; +- if (question === void 0) return null; +- const onAnswer = (answer) => { +- const next = [...answers, { id: question.id, ...answer }]; +- if (index + 1 < questions.length) { +- setAnswers(next); +- setIndex(index + 1); +- return; + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -5273,21 +7654,19 @@ diff --git a/client.js b/client.js + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; - } - - // 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 +4559,71 @@ function elapsedLabel(ms) { - const hours = Math.floor(minutes / 60); - return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { ++var import_react_dom2 = require("react-dom"); ++var PHRASE_HOLD_MS = 800; ++var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); ++function elapsedLabel(ms) { ++ const minutes = Math.floor(ms / 6e4); ++ if (minutes < 60) return `${Math.max(1, minutes)}m`; ++ const hours = Math.floor(minutes / 60); ++ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++} +var RUNNING_HOST = "[data-chat-flow] > [data-chat-running]"; +var TAIL_HOST = "tail"; +var REMOTE_GRACE_MS = 1500; @@ -5327,7 +7706,10 @@ diff --git a/client.js b/client.js + sendTurn.current = -1; + setReplied(false); + return void 0; -+ } + } +- pending.answer({ answers: next }); +- }; +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); + if (wasSending.current) { + wasSending.current = false; + sendTurn.current = lastTurnOnScreen(); @@ -5349,49 +7731,102 @@ diff --git a/client.js b/client.js + }; + }, [remote, sending]); + return replied; -+} + } +-function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); +- if (answered === question.id) return null; +- const onAnswer = (answer) => { +- setAnswered(question.id); +- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); +- }; +- const onDismiss = () => { +- setAnswered(question.id); +- void actions.dismissQuestion(sessionId); +- }; +- return (0, import_react4.createElement)( +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react10.useState)(null); + (0, import_react10.useEffect)(() => { - if (!active) { - setHost(null); - return void 0; -@@ -4569,7 +4569,7 @@ function useRunningHost(active) { - let frame = 0; - const find = () => { - frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); ++ if (!active) { ++ setHost(null); ++ return void 0; ++ } ++ let frame = 0; ++ const find = () => { ++ frame = 0; + const node = findHost(where); - setHost((prev) => prev === node ? prev : node); - }; - const observer = new MutationObserver(() => { -@@ -4581,13 +4581,13 @@ function useRunningHost(active) { - cancelAnimationFrame(frame); - observer.disconnect(); - }; -- }, [active]); ++ setHost((prev) => prev === node ? prev : node); ++ }; ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(find); ++ }); ++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); ++ find(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; + }, [active, where]); - return host; - } - function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); ++ return host; ++} ++function useRowEntrances(replacing) { + const check = (0, import_react10.useRef)(replacing); - check.current = replacing; -- (0, import_react7.useEffect)(() => { ++ check.current = replacing; + (0, import_react10.useEffect)(() => { - const seen = /* @__PURE__ */ new WeakSet(); - const started = Date.now(); - let column = null; -@@ -4638,21 +4638,31 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); - const bot = roster.byId[sessionId]; - const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); ++ const seen = /* @__PURE__ */ new WeakSet(); ++ const started = Date.now(); ++ let column = null; ++ let frame = 0; ++ const settle = (event) => { ++ if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; ++ delete event.currentTarget.dataset.btEnter; ++ event.currentTarget.removeEventListener("animationend", settle); ++ }; ++ const scan = () => { ++ frame = 0; ++ const next = document.querySelector("[data-chat-flow]"); ++ if (next !== column) { ++ observer.disconnect(); ++ column = next; ++ if (column) observer.observe(column, { childList: true, subtree: true }); ++ } ++ if (!column) return; ++ const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; ++ let last = -1; ++ items.forEach((item, index) => { ++ if (seen.has(item)) last = index; ++ }); ++ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); ++ for (const item of fresh) seen.add(item); ++ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; ++ for (const item of fresh) { ++ if (items.indexOf(item) < last) continue; ++ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); ++ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; ++ item.addEventListener("animationend", settle); ++ } ++ }; ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(scan); ++ }); ++ scan(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; ++ }, []); ++} ++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; @@ -5402,49 +7837,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,24 +4663,29 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const timer = setTimeout(make, wait); - return () => clearTimeout(timer); - }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); ++ if (wantKey === null || shown?.key === wantKey) return void 0; ++ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); ++ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; ++ if (wait <= 0) { ++ make(); ++ return void 0; ++ } ++ const timer = setTimeout(make, wait); ++ return () => clearTimeout(timer); ++ }, [wantKey, shown]); + const [phase, setPhase] = (0, import_react10.useState)(null); + const endedAt = (0, import_react10.useRef)(0); + const shift = (0, import_react10.useRef)(null); + const leaving = (0, import_react10.useRef)(false); - leaving.current = phase !== null && !visible; -- (0, import_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); @@ -5455,31 +7878,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(); - setShown(null); -@@ -4691,31 +4691,37 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us - const delta = before - scroller.scrollTop; - if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); - }, [phase]); -- (0, import_react7.useEffect)(() => { ++ if (phase !== null) return; ++ endedAt.current = Date.now(); ++ setShown(null); ++ const before = shift.current; ++ shift.current = null; ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ if (before === null || !scroller || reducedMotion()) return; ++ const delta = before - scroller.scrollTop; ++ if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); ++ }, [phase]); + (0, import_react10.useEffect)(() => { - if (!running) actions.clearLive(sessionId); - }, [running, sessionId]); - useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { ++ if (!running) actions.clearLive(sessionId); ++ }, [running, sessionId]); ++ useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); + const [now, setNow] = (0, import_react10.useState)(Date.now); + (0, import_react10.useEffect)(() => { - if (phase !== "in") return void 0; - const timer = setInterval(() => setNow(Date.now()), 1e4); - return () => clearInterval(timer); - }, [phase]); -- const 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); @@ -5487,40 +7908,205 @@ 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: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) +- ); +-} +-function 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 }); + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react10.createElement)( - "span", - { className: "bt-typing-text" }, -- (0, import_react7.createElement)( ++ "span", ++ { className: "bt-typing-text" }, + (0, import_react10.createElement)( - "span", - { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, -- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), -- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null ++ "span", ++ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null - ) - ) - ), host); -@@ -4731,49 +4731,1945 @@ function showsDshRow(dsh, app) { - return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ ) ++ ) ++ ), host); + } +-function AnsweredCell({ node }) { +- const data = node.data ?? {}; +- const questions = Array.isArray(data.questions) ? data.questions : []; +- if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; +- } +- const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- key: question.id, +- question, +- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +- }))); ++ ++// src/client/dsh-offer.js ++var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); ++var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; ++var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); ++function showsDshRow(dsh, app) { ++ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; ++ if (dsh.kind === "npm") return true; ++ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } +-// src/client/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 ")); +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; +var onEscapeKey = (event) => { + if (event.key === "Escape") escapeLayers.at(-1)?.current(); -+}; + }; +-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; +-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +- } +- }); +-} +-function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { +- key: id, +- type: "button", +- role: "radio", +- "aria-checked": value === id, +- className: "bt-secret-seg", +- onClick: () => onChange(id) +- }, text)) +- ); +-} +-function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") +- ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null +- ); +-} +-function SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); +- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); +- const run = (promise) => { +- setBusy(true); +- setError(""); +- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); +- }; +- const cancel = () => run(actions.secretCancel(request.id)); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); +- }; +- const allow = () => { +- if (!busy) void run(actions.secretAllow(request.id)); +- }; +- const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { +- bot, +- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), +- detail: request.purpose, +- name: request.name, +- onClose: busy ? void 0 : cancel +- }), +- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, +- filling ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-body" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), +- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null +- ) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-note", id: noteId }, +- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), +- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), +- ".", +- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" +- ), +- warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-foot" }, +- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), +- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) +- ) +- ); +function useEscape(onClose) { + const ref = (0, import_react11.useRef)(onClose); + ref.current = onClose; @@ -5532,7 +8118,26 @@ diff --git a/client.js b/client.js + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); + }; + }, []); -+} + } +-function SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), +- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( +- "p", +- { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) +- )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) +- ) +- ); +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -5540,7 +8145,9 @@ diff --git a/client.js b/client.js + if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); + }, []); + return paneLeft; -+} + } +-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 }); + +// src/client/member-status.js +var import_react12 = require("react"); @@ -5563,23 +8170,103 @@ diff --git a/client.js b/client.js + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; + }); -+} + } +-function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { +- const bot = roster.byId[sessionId]; +- if (!bot) return null; +- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); +- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); +- if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); +- } +- return null; +function 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 SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { +- const roster = useRoster((value) => value); +- const shown = useCards?.((state) => state[sessionId]) ?? NONE; +- const loading = useSession?.(historyLoading) === true; +- const bot = roster.byId[sessionId]; +- if (!bot || currentPart(bot) !== sessionId) return null; +- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); +- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); +- if (waiting.length === 0) return null; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); + return { rows, rest: busy.filter((entry) => !rows.includes(entry)) }; -+} + } +-function 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 botGroupWork(botId, work, roster) { + return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => { + const entry = entries2.find((item) => item.botId === botId); + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; + }); -+} + } +-function 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 summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -5588,14 +8275,82 @@ diff --git a/client.js b/client.js + const first = workers[0]; + const doing = first ? `${first.bot.name} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; + return { waiting, doing }; -+} + } +-function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) +- ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null +function Face({ entry, size }) { + return (0, import_react12.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) -+ ); -+} + ); + } +-function 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 elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +function WorkRow({ bot, tool, step, label, at, now, onOpen }) { + const text = `${label} · ${workText(tool, step)}`; @@ -5606,8 +8361,28 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }), + (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text)), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -+ ); -+} + ); + } +-function 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 +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -5621,10 +8396,151 @@ diff --git a/client.js b/client.js + waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, + waiting && doing ? " · " : null, + doing ? (0, import_react12.createElement)(Shimmer, null, doing) : 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"); +-} +-var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); +-var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); +-var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); +- +-// src/client/cells.js +-var import_react6 = require("react"); +-var import_react_dom = require("react-dom"); +-var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); +-var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); +-function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { +- const onClick = (event) => { +- const target = mentionTarget(event); +- if (target === null) return; +- event.preventDefault(); +- event.stopPropagation(); +- actions.openSession(target); +- }; +- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) +- ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); +- }) + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -+ ); -+} + ); + } +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -5669,13 +8585,43 @@ diff --git a/client.js b/client.js + const column = flow.getBoundingClientRect(); + node.style.marginInlineStart = `${Math.round(column.left - node.getBoundingClientRect().left)}px`; + node.style.width = `${Math.round(column.width)}px`; -+ }; + }; +- }, [open]); + place(); + const observer = new ResizeObserver(place); + for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); + return () => observer.disconnect(); + }, [active, key]); -+} + } +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; +- } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); +- }); +- }; +- const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); +- link.click(); +- setTimeout(() => URL.revokeObjectURL(url), 1e3); +function useNow(active) { + const [now, setNow] = (0, import_react12.useState)(Date.now); + (0, import_react12.useEffect)(() => { @@ -5695,27 +8641,54 @@ diff --git a/client.js b/client.js + const answer = (choice) => { + setBusy(true); + void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); -+ }; + }; +- 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)( + return (0, import_react12.createElement)( -+ "div", + "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: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react12.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react12.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, + approval.reason ? (0, import_react12.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null, + approval.detail?.code ? (0, import_react12.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null, + approval.answerable ? (0, import_react12.createElement)( -+ "div", + "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)( + { className: "bt-mw-actions" }, + (0, import_react12.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")), + (0, import_react12.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")), + ...alwaysChoices(approval, bot).map(([choice, label]) => (0, import_react12.createElement)("button", { key: choice, type: "button", className: "bt-q-btn", "data-choice": choice, disabled: busy, onClick: () => answer(choice) }, t(label))) + ) : (0, import_react12.createElement)( -+ "div", + "div", +- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, +- item(t("Copy"), CopyIcon, copy2), +- item(t("Download"), DownloadIcon, download2) +- ), document.body) : null + { className: "bt-mw-actions" }, + (0, import_react12.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) + ) -+ ); -+} + ); + } +-function 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 MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -5740,14 +8713,57 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("span", null, (0, import_react12.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) -+ ); -+} + ); + } +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; +- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; +- }, [fileMentions, openFile, seq, tail, turn]); +function 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 AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- const running = data.status === "running"; +- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); +- if (hidden) return null; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; +- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true +- }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +- ); +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} @@ -5843,7 +8859,20 @@ diff --git a/client.js b/client.js + }; + }, []); + return tasks; -+} + } +-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) +- ) +- ); +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -5861,7 +8890,17 @@ diff --git a/client.js b/client.js + if (question == null || typeof question !== "object" || roster.roomsById?.[key]) continue; + if (question.question !== text) continue; + if (key === bot.id || key === currentPart(bot)) return bot.id; -+ } + } +- 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 void 0; +}; +var placeKey = (botId, roomId) => `${botId}|${roomId ?? ""}`; @@ -5945,7 +8984,8 @@ diff --git a/client.js b/client.js + title: count === 1 ? t("1 message waits for your approval") : t("{count} messages wait for your approval", { count }), + hint: t("Approve it in the chat"), + ...extra -+ }); + }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + for (const bot of roster.bots ?? []) { + const count = bot.pendingCount ?? 0; + if (count <= 0) continue; @@ -6183,7 +9223,9 @@ diff --git a/client.js b/client.js + }), + (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); -+} + } +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { + const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; @@ -6192,7 +9234,9 @@ diff --git a/client.js b/client.js + const count = needsCount(roster, work, tasks); + const label = count > 0 ? t("Inbox, {count} need you", { count }) : t("Inbox"); + return (0, import_react13.createElement)( -+ "button", + "button", +- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) + { + type: "button", + className: "bt-row bt-inbox-row", @@ -6205,7 +9249,8 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("span", { className: "bt-inbox-mark" }, (0, import_react13.createElement)(InboxIcon)), + wide ? (0, import_react13.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null -+ ); + ); +- return (0, import_react6.createElement)( +} +function Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; @@ -6228,11 +9273,16 @@ diff --git a/client.js b/client.js + const writeBack = card.kind === "task" && card.task?.writeBack?.state === "failed" ? card.task.writeBack : void 0; + const unconfirmed = card.kind === "task" && card.task?.writeBack?.state === "unconfirmed" ? card.task.writeBack : void 0; + return (0, import_react13.createElement)( -+ "div", + "div", +- { className: "bt-group-msg" }, +- (0, import_react6.createElement)( + { className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status }, + (0, import_react13.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react13.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react13.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null), + (0, import_react13.createElement)( -+ "span", + "span", +- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, +- bot.name, +- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + { className: "bt-inbox-copy" }, + (0, import_react13.createElement)( + "span", @@ -6266,7 +9316,8 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)(AnswerFields, { card, busy, onAnswer }), + (0, import_react13.createElement)(ResultActions, { card, busy, onAnswer }), + error ? (0, import_react13.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null -+ ), + ), +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + (0, import_react13.createElement)( + "span", + { className: "bt-inbox-actions" }, @@ -6275,8 +9326,15 @@ diff --git a/client.js b/client.js + ...(answerable ? alwaysChoices(card.approval, card.bot) : []).map(([choice, label]) => (0, import_react13.createElement)("button", { key: choice, type: "button", className: "bt-soft bt-soft-sm", "data-choice": choice, disabled: busy, onClick: () => onAnswer(card, choice) }, t(label))), + (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) -+ ); -+} + ); + } +-function 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 useNow2(ms) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -6284,7 +9342,16 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [ms]); + return now; -+} + } +-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- const target = other && other.id !== self?.id ? other : self; +- if (!target) return null; +- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( +function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { + const work = useWork((value) => value); + const sessions = useSessions((value) => value); @@ -6322,7 +9389,9 @@ diff --git a/client.js b/client.js + actions.openSession(card.chat); + }; + return (0, import_react13.createElement)( -+ "div", + "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) + { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 }, + (0, import_react13.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react13.createElement)( @@ -6362,8 +9431,10 @@ diff --git a/client.js b/client.js + )) + ) + ) -+ ); -+} + ); + } +-function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +var 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)} @@ -6414,48 +9485,488 @@ 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 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/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 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))); + } +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; ++ ++// src/client/delivery-chart.js ++var import_react14 = require("react"); ++var CHART_WIDTH = 168; ++var CHART_HEIGHT = 28; ++var PAD = 3; ++var DOT_RADIUS = 2; ++var HOLLOW_RADIUS = 2.5; ++var CHART_DAYS = 7; ++var CHART_KEYS = ["newIssues", "closedIssues", "newPullRequests", "mergedPullRequests", "ci"]; ++var COUNTING_KEYS = ["newIssues", "closedIssues", "newPullRequests", "mergedPullRequests"]; ++function chartDays(daily) { ++ if (!Array.isArray(daily)) return null; ++ const days = daily.filter((day) => day && typeof day === "object"); ++ return days.length ? days.slice(-CHART_DAYS) : null; ++} ++function todayPartial(daily) { ++ const days = chartDays(daily); ++ return Boolean(days && days.length && days[days.length - 1].partial === true); ++} ++function dayValue(day, key) { ++ if (!day || typeof day !== "object") return null; ++ if (key === "ci") { ++ const failed = day.ci?.failed; ++ const counted = day.ci?.counted; ++ const total = day.ci?.total; ++ const denominator = Number.isFinite(counted) && counted > 0 ? counted : Number.isFinite(total) && total > 0 ? total : null; ++ if (!Number.isFinite(failed) || denominator === null) return null; ++ return Math.min(1, Math.max(0, failed / denominator)); + } +- return null; +-}; +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +- }); +- }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); +- return () => { +- alive.current = false; ++ const value = day[key]; ++ return Number.isFinite(value) ? value : null; ++} ++function dayLabel2(day) { ++ if (typeof day?.date === "string" && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(day.date)) return day.date; ++ const at = Date.parse(day?.start ?? ""); ++ if (!Number.isFinite(at)) return ""; ++ return new Date(at + 8 * 36e5).toISOString().slice(0, 10); ++} ++function pointTitle(day, key) { ++ const date = dayLabel2(day); ++ const value = dayValue(day, key); ++ const shown = value === null ? t("not available") : key === "ci" ? `${Math.round(value * 100)}%` : String(value); ++ return date ? `${date} · ${shown}` : shown; ++} ++function knownDays(daily, key) { ++ const days = chartDays(daily) ?? []; ++ return days.filter((day) => dayValue(day, key) !== null).length; ++} ++var round = (value) => Math.round(value * 100) / 100; ++function chartGeometry(daily, key, { width = CHART_WIDTH, height = CHART_HEIGHT } = {}) { ++ const days = chartDays(daily); ++ if (!days) return null; ++ const values = days.map((day) => dayValue(day, key)); ++ const known = values.filter((value) => value !== null); ++ const top = known.length ? Math.max(...known, 0) : 0; ++ const span = top > 0 ? top : 1; ++ const step = days.length > 1 ? (width - PAD * 2) / (days.length - 1) : 0; ++ const dots = days.map((day, i) => { ++ const value = values[i]; ++ const last = i === days.length - 1; ++ const state = value === null ? "missing" : last && day.partial === true ? "partial" : "full"; ++ return { ++ index: i, ++ x: round(PAD + step * i), ++ y: value === null ? null : round(PAD + (height - PAD * 2) * (1 - value / span)), ++ today: last, ++ // Only the day that is not over yet is drawn hollow; a finished day is a filled dot. ++ hollow: state === "partial", ++ partial: state === "partial", ++ state, ++ title: pointTitle(day, key), ++ value + }; +- }, [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) : ""; + }); +- 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); ++ const segments = []; ++ let run = null; ++ for (const dot of dots) { ++ if (dot.y === null) { ++ if (run) { ++ segments.push(run); ++ run = null; + } +- return (0, import_react6.createElement)(EventLine, { +- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), +- bots, +- onOpen: (bot) => actions.openExchange(selfId, bot.id), +- whole: true +- }); +- } +- if (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")); ++ continue; + } +- 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 TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- const own = state[sessionId]?.[turn]; +- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; +- }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; +-} +-function Nothing() { +- return null; +-} +-var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +-function clockLabel(time, now = Date.now()) { +- const date = new Date(time); +- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); +- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); +- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); +- if (days === 0) return t("Today {time}", { time: clock4 }); +- if (days === 1) return t("Yesterday {time}", { time: clock4 }); +- const sameYear = date.getFullYear() === new Date(now).getFullYear(); +- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +-} +-function nextTurnStart(turns, turn) { +- let next; +- for (const key in turns) { +- const candidate = Number(key); +- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; ++ if (!run) run = { dots: [] }; ++ run.dots.push(dot); + } +- return next === void 0 ? void 0 : turns[next].first; ++ if (run) segments.push(run); ++ return { ++ width, ++ height, ++ dots, ++ known: known.length, ++ segments: segments.map((segment) => ({ ++ d: segment.dots.map((dot, i) => `${i ? "L" : "M"}${dot.x} ${dot.y}`).join(" "), ++ // Only the stretch that ends today is the day that is not over yet. ++ dashed: segment.dots.some((dot) => dot.partial), ++ today: segment.dots.some((dot) => dot.today) ++ })) ++ }; + } +-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 DeliveryChart({ daily, metricKey, label }) { ++ const geo = chartGeometry(daily, metricKey); ++ if (!geo || !geo.known) return null; ++ const name = label ? t("{label} over the last 7 days", { label }) : t("Last 7 days"); ++ return (0, import_react14.createElement)( ++ "svg", ++ { ++ className: "bt-del-chart", ++ "data-flybot": "delivery-chart", ++ "data-metric": metricKey, ++ width: geo.width, ++ height: geo.height, ++ viewBox: `0 0 ${geo.width} ${geo.height}`, ++ role: "img", ++ "aria-label": name ++ }, ++ geo.segments.map((segment, i) => (0, import_react14.createElement)("path", { ++ key: `s${i}`, ++ className: "bt-del-chart-line", ++ "data-today": segment.today ? "true" : void 0, ++ ...segment.dashed ? { "stroke-dasharray": "2 2" } : {}, ++ d: segment.d ++ })), ++ geo.dots.filter((dot) => dot.y !== null).map((dot) => (0, import_react14.createElement)("circle", { ++ key: `d${dot.index}`, ++ className: "bt-del-chart-dot", ++ "data-state": dot.state, ++ "data-hollow": dot.hollow ? "true" : void 0, ++ cx: dot.x, ++ cy: dot.y, ++ r: dot.hollow ? HOLLOW_RADIUS : DOT_RADIUS ++ }, (0, import_react14.createElement)("title", null, dot.title))) ++ ); + } +-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; ++function DeliveryMetrics({ rows = [], daily }) { ++ const days = chartDays(daily); ++ const short = CHART_KEYS.map((key) => knownDays(days, key)).filter((count) => count < (days?.length ?? 0)); ++ const fewest = short.length ? Math.min(...short) : 0; ++ const head = (0, import_react14.createElement)( ++ "div", ++ { className: "bt-del-dm-head", "data-flybot": "delivery-metrics-head" }, ++ (0, import_react14.createElement)("span", { className: "bt-del-dm-label" }, ""), ++ (0, import_react14.createElement)("span", { className: "bt-del-dm-chart-head" }, ""), ++ (0, import_react14.createElement)("span", { className: "bt-del-dm-value" }, t("Today")), ++ (0, import_react14.createElement)("span", { className: "bt-del-dm-value bt-del-dm-week" }, t("7 days")) ++ ); ++ const body = rows.map((row, index) => { ++ const key = CHART_KEYS.includes(row?.key) ? row.key : null; ++ const chart = days && key ? (0, import_react14.createElement)(DeliveryChart, { key: `c-${key}`, daily: days, metricKey: key, label: row?.label }) : null; ++ const counted = key !== null && COUNTING_KEYS.includes(key); ++ const star = days && counted && knownDays(days, key) < days.length ? "*" : ""; ++ const cells = [ ++ (0, import_react14.createElement)("span", { key: "label", className: "bt-del-dm-label" }, row?.label ?? ""), ++ // A median row gets no chart cell at all: no line, no placeholder box, no total. ++ ...key ? [(0, import_react14.createElement)("span", { key: "chart", className: "bt-del-dm-chart" }, chart)] : [], ++ (0, import_react14.createElement)("span", { key: "today", className: "bt-del-dm-value bt-del-dm-today" }, row?.today ?? "—"), ++ (0, import_react14.createElement)("span", { key: "week", className: "bt-del-dm-value bt-del-dm-week" }, counted ? `Σ ${row?.week ?? "—"}${star}` : row?.week ?? "—") ++ ]; ++ return (0, import_react14.createElement)("div", { key: row?.label ?? String(index), className: "bt-del-dm-row", "data-metric": key ?? void 0 }, cells); + }); +- 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 ++ return (0, import_react14.createElement)( ++ "div", ++ { className: "bt-del-dm", "data-flybot": "delivery-metrics" }, ++ head, ++ body, ++ days ? null : (0, import_react14.createElement)("p", { className: "bt-del-chart-empty", "data-flybot": "delivery-chart-empty" }, t("No 7-day data yet.")), ++ days && short.length ? (0, import_react14.createElement)("p", { className: "bt-del-chart-foot", "data-flybot": "delivery-chart-foot" }, t("* {count} of {days} days had data", { count: fewest, days: days.length })) : null + ); + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); ++var DELIVERY_CHART_CSS = ` ++.bt-del-dm{display:flex;flex-direction:column;gap:2px;min-width:0} ++.bt-del-dm-head,.bt-del-dm-row{display:flex;align-items:center;gap:8px} ++.bt-del-dm-head{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)} ++.bt-del-dm-label{flex:1 1 auto;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-2)} ++.bt-del-dm-head .bt-del-dm-label{color:inherit} ++.bt-del-dm-chart,.bt-del-dm-chart-head{flex:0 0 168px;display:flex;align-items:center;min-height:28px} ++.bt-del-dm-value{flex:0 0 auto;min-width:56px;text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;color:var(--bt-ink)} ++.bt-del-dm-head .bt-del-dm-value{color:inherit;font-weight:inherit} ++.bt-del-dm-today{color:var(--bt-ink);font-weight:600} ++.bt-del-dm-week{color:var(--bt-ink-3);font-weight:400} ++.bt-del-chart{display:block;overflow:visible} ++.bt-del-chart-line{fill:none;stroke:var(--bt-accent);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round} ++.bt-del-chart-dot{fill:var(--bt-accent);stroke:none} ++.bt-del-chart-dot[data-state=partial]{fill:none;stroke:var(--bt-accent);stroke-width:1.5} ++.bt-del-chart-empty,.bt-del-chart-foot{margin:2px 0 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} ++@media (max-width:899.98px){ ++ .bt-del-dm-head{display:none} ++ .bt-del-dm-row{flex-wrap:wrap;gap:4px 8px;min-height:44px;align-content:center} ++ .bt-del-dm-chart{order:5;flex:0 0 100%;min-height:0} ++ .bt-del-dm-value{min-width:0} + } +-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"] ++`; ++ +// src/client/delivery-view.js -+var import_react14 = require("react"); ++var import_react15 = require("react"); +var POLL_MS3 = 6e4; +var LOADING_POLL_MS = 2e3; +var RETRY_FLOOR_MS = 1e3; @@ -6474,7 +9985,27 @@ diff --git a/client.js b/client.js + { id: "working", label: "In progress" }, + { id: "review", label: "In review" }, + { id: "done-today", label: "Done today" } -+]; + ]; +-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" }; +var BADGES = { + inc: "INC · rework", + "pass-enqueue": "PASS · waiting for enqueue", @@ -6544,17 +10075,20 @@ diff --git a/client.js b/client.js + const today = snapshot?.today ?? {}; + const week = snapshot?.week ?? {}; + const ci = today.ci ?? null; ++ const share = (entry) => entry && Number.isFinite(entry.counted) && entry.counted > 0 ? `${Math.round(entry.failed / entry.counted * 100)}%` : "—"; + const count = (value) => value === null || value === void 0 ? "—" : String(value); + const rows = [ + { label: t("New issues"), key: "newIssues" }, + { label: t("Closed issues"), key: "closedIssues" }, + { label: t("New pull requests"), key: "newPullRequests" }, + { label: t("Merged pull requests"), key: "mergedPullRequests" }, ++ { key: "ci", label: t("CI red share"), today: share(today.ci), week: share(week.ci) }, + { label: t("Median issue close"), key: "medianIssueCloseMs", format: duration }, + { label: t("Median PR merge"), key: "medianPullMergeMs", format: duration } + ].map((row) => { ++ if (row.today !== void 0) return { key: row.key, label: row.label, today: row.today, week: row.week }; + const format = row.format ?? count; -+ return { label: row.label, today: format(today[row.key]), week: format(week[row.key]) }; ++ return { key: row.key, label: row.label, today: format(today[row.key]), week: format(week[row.key]) }; + }); + return { + dayStart: snapshot?.dayStart ?? null, @@ -6604,7 +10138,12 @@ diff --git a/client.js b/client.js + last = null; + onView({ state: "failed", error: { message: String(error?.message ?? error), kind: "network", endpoint: null, status: null, at: null } }); + schedule(); -+ } + } +- 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" }; + }; + return { + start: () => { @@ -6627,14 +10166,30 @@ diff --git a/client.js b/client.js + stopped = true; + if (timer !== null) clearTimer(timer); + timer = null; -+ } + } +- 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 useDeliveryBoard(actions) { -+ const [view, setView] = (0, import_react14.useState)({ state: "idle" }); -+ const [retry, setRetry] = (0, import_react14.useState)(() => () => { ++ const [view, setView] = (0, import_react15.useState)({ state: "idle" }); ++ const [retry, setRetry] = (0, import_react15.useState)(() => () => { + }); -+ (0, import_react14.useEffect)(() => { ++ (0, import_react15.useEffect)(() => { + let current = { state: "idle" }; + const poller = createPoller({ + load: (force) => typeof actions?.deliveryBoard === "function" ? actions.deliveryBoard(force ? { force: true } : {}) : Promise.resolve(null), @@ -6655,13 +10210,16 @@ diff --git a/client.js b/client.js + for (const row of report.rows ?? []) { + const status = row.status === null || row.status === void 0 ? "—" : String(row.status); + lines.push(`${row.ok ? "ok" : "FAIL"} ${status.padEnd(4)} ${row.endpoint}${row.ok ? "" : ` ${row.kind ?? ""}`}`); -+ } + } + if (report.error) lines.push(String(report.error)); + return lines.join("\n"); -+} + } +-function workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +function Diagnose({ onProbe }) { -+ const [report, setReport] = (0, import_react14.useState)(null); -+ const [busy, setBusy] = (0, import_react14.useState)(false); ++ const [report, setReport] = (0, import_react15.useState)(null); ++ const [busy, setBusy] = (0, import_react15.useState)(false); + const run = () => { + if (busy) return; + setBusy(true); @@ -6669,15 +10227,15 @@ diff --git a/client.js b/client.js + setReport(next ?? { rows: [], error: "no answer" }); + }).catch((error) => setReport({ rows: [], error: String(error?.message ?? error) })).finally(() => setBusy(false)); + }; -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "span", + { className: "bt-del-diag" }, -+ (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: run, "aria-label": t("Diagnose") }, busy ? t("Checking…") : t("Diagnose")), -+ report ? (0, import_react14.createElement)( ++ (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: run, "aria-label": t("Diagnose") }, busy ? t("Checking…") : t("Diagnose")), ++ report ? (0, import_react15.createElement)( + "div", + { className: "bt-del-diag-box" }, -+ (0, import_react14.createElement)("pre", { className: "bt-del-diag-text", "data-probe": "report" }, probeText(report)), -+ (0, import_react14.createElement)("button", { ++ (0, import_react15.createElement)("pre", { className: "bt-del-diag-text", "data-probe": "report" }, probeText(report)), ++ (0, import_react15.createElement)("button", { + type: "button", + className: "bt-soft bt-soft-sm", + onClick: () => { @@ -6687,36 +10245,33 @@ diff --git a/client.js b/client.js + } + } + }, t("Copy")), -+ (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setReport(null) }, t("Close")) ++ (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setReport(null) }, t("Close")) + ) : null + ); -+} + } +-function useLiveNote(actions, sessionId, id, entry) { +- const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { +- if (!key) return void 0; +- actions.noteLive(sessionId, id, JSON.parse(key)); +- return () => actions.noteLive(sessionId, id, null); +- }, [sessionId, id, key]); +function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, onProbe = null }) { + const payload = view ?? snapshot; + if (!payload) return null; -+ const { rows, ci, open } = metricRows(payload.metrics ?? payload); ++ const metricsPayload = payload.metrics ?? payload; ++ const { rows, ci, open } = metricRows(metricsPayload); + const error = payload.error ?? null; + const state = payload.state ?? "fresh"; -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "header", + { className: "bt-del-head" }, -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)( + "div", + { className: "bt-del-metrics" }, -+ (0, import_react14.createElement)( -+ "table", -+ { className: "bt-del-table" }, -+ (0, import_react14.createElement)("thead", null, (0, import_react14.createElement)("tr", null, (0, import_react14.createElement)("th", null, ""), (0, import_react14.createElement)("th", null, t("Today")), (0, import_react14.createElement)("th", null, t("7 days")))), -+ (0, import_react14.createElement)("tbody", null, rows.map((row) => (0, import_react14.createElement)( -+ "tr", -+ { key: row.label }, -+ (0, import_react14.createElement)("th", { scope: "row" }, row.label), -+ (0, import_react14.createElement)("td", null, row.today), -+ (0, import_react14.createElement)("td", null, row.week) -+ ))) -+ ), ++ (0, import_react15.createElement)(DeliveryMetrics, { rows, daily: metricsPayload.daily }), + // An unreadable CI number is "unknown", never "0 of 0" (#1211). -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)( + "p", + { className: "bt-del-ci", "data-ci": ci === null ? "unknown" : "read" }, + ci === null ? t("CI not available this time") : t("CI failed {failed} of {total}", { failed: ci.failed, total: ci.total }), @@ -6724,30 +10279,62 @@ diff --git a/client.js b/client.js + ci !== null && ci.superseded ? ` · ${t("{count} superseded", { count: ci.superseded })}` : "", + ci !== null && ci.unknownDays?.length ? ` · ${t("{count} day(s) not read", { count: ci.unknownDays.length })}` : "" + ), -+ (0, import_react14.createElement)("p", { className: "bt-del-open" }, t("Open: {issues} issues ({needs} need a human), {prs} pull requests", { ++ (0, import_react15.createElement)("p", { className: "bt-del-open" }, t("Open: {issues} issues ({needs} need a human), {prs} pull requests", { + issues: open.issues ?? 0, + needs: open.needsHuman ?? 0, + prs: open.pullRequests ?? 0 + }), open.medianIssueAgeMs !== null && open.medianIssueAgeMs !== void 0 ? ` · ${t("median open age {age}", { age: duration(open.medianIssueAgeMs) })}` : "") + ), -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)( + "p", + { className: "bt-del-asof" }, + state === "loading" ? loadingText(payload.elapsedMs) : t("as of {time}", { time: timeOf(payload.asOf) }), ++ state !== "loading" && todayPartial(metricsPayload.daily) ? ` · ${t("today is partial · as of {time}", { time: clockOf(payload.asOf) })}` : "", + payload.refreshing ? ` · ${t("refreshing…")}` : "", + error ? ` · ${hintFor(error, { repo })}` : "", -+ onRetry ? (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null, -+ onProbe ? (0, import_react14.createElement)(Diagnose, { onProbe }) : null ++ onRetry ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null, ++ onProbe ? (0, import_react15.createElement)(Diagnose, { onProbe }) : null + ) + ); -+} + } +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +- if (done) return void 0; +- let stop = false; +- let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +- } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); +- }; +- void tick(); +- return () => { +- stop = true; +- clearTimeout(timer); +- }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { +- try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; +- } +function DeliveryBoard({ view, snapshot, message = "", repo = null, onRetry = null, onProbe = null }) { + const payload = view ?? (snapshot ? { ...snapshot, state: "fresh" } : { state: "idle" }); + const state = payload.state ?? "fresh"; + const retryAt = Number(payload.retryAt); + const ticking = (state === "failed" || state === "stale") && Number.isFinite(retryAt) && retryAt > 0; -+ const [now, setNow] = (0, import_react14.useState)(() => Date.now()); -+ (0, import_react14.useEffect)(() => { ++ const [now, setNow] = (0, import_react15.useState)(() => Date.now()); ++ (0, import_react15.useEffect)(() => { + if (!ticking) return void 0; + setNow(Date.now()); + const timer = setInterval(() => setNow(Date.now()), 1e3); @@ -6755,74 +10342,139 @@ diff --git a/client.js b/client.js + }, [ticking, retryAt]); + const countdown = retryCountdown(payload.retryAt, now); + if (state === "idle") { -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "div", + { className: "bt-del-body", "data-view": "delivery", "data-state": state }, -+ message ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("No snapshot yet.")) ++ message ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("No snapshot yet.")) + ); -+ } + } +- 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 (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)( + if (state === "loading") { -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "div", + { className: "bt-del-body", "data-view": "delivery", "data-state": state }, -+ (0, import_react14.createElement)("p", { className: "bt-del-loading", role: "status" }, loadingText(payload.elapsedMs)), -+ (0, import_react14.createElement)("p", { className: "bt-del-hint" }, t("The first round reads the repository once; the columns appear as soon as it lands.")) ++ (0, import_react15.createElement)("p", { className: "bt-del-loading", role: "status" }, loadingText(payload.elapsedMs)), ++ (0, import_react15.createElement)("p", { className: "bt-del-hint" }, t("The first round reads the repository once; the columns appear as soon as it lands.")) + ); + } + if (state === "failed" || !payload.columns) { + const error = payload.error ?? null; -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "div", + { className: "bt-del-body", "data-view": "delivery", "data-state": state }, -+ (0, import_react14.createElement)( -+ "div", ++ (0, import_react15.createElement)( + "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); +- }) + { className: "bt-board-error", role: "alert" }, -+ (0, import_react14.createElement)("p", null, String(error?.message ?? message ?? t("The delivery board is not ready yet."))), -+ error ? (0, import_react14.createElement)("p", { className: "bt-del-hint" }, hintFor(error, { repo })) : null, -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)("p", null, String(error?.message ?? message ?? t("The delivery board is not ready yet."))), ++ error ? (0, import_react15.createElement)("p", { className: "bt-del-hint" }, hintFor(error, { repo })) : null, ++ (0, import_react15.createElement)( + "p", + { className: "bt-del-hint", "data-retry-at": payload.retryAt ?? void 0 }, + countdown === null ? t("While this page is open it retries on its own; press Retry to try again now.") : t("Retrying in {seconds} s · press Retry to try again now", { seconds: countdown }) + ), -+ onRetry ? (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null ++ onRetry ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-del-retry", onClick: onRetry }, t("Retry")) : null + ) + ); + } + const columns = payload.columns ?? {}; + const missing = payload.missing ?? []; -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "div", + { className: "bt-del-body", "data-view": "delivery", "data-state": state }, -+ message ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : null, -+ state === "stale" && payload.error ? (0, import_react14.createElement)( ++ message ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, message) : null, ++ state === "stale" && payload.error ? (0, import_react15.createElement)( + "p", + { className: "bt-del-stale", role: "status" }, + staleText(payload.asOf, payload.error), + ` · ${hintFor(payload.error, { repo })}`, + countdown === null ? "" : ` · ${t("Retrying in {seconds} s · press Retry to try again now", { seconds: countdown })}` + ) : null, -+ state !== "stale" && missing.length > 0 ? (0, import_react14.createElement)("p", { className: "bt-del-partial", role: "status" }, missing.map((entry) => missingText(entry)).join(" · ")) : null, -+ (0, import_react14.createElement)(DeliveryHeader, { view: payload, repo, onRetry, onProbe }), -+ (0, import_react14.createElement)( ++ state !== "stale" && missing.length > 0 ? (0, import_react15.createElement)("p", { className: "bt-del-partial", role: "status" }, missing.map((entry) => missingText(entry)).join(" · ")) : null, ++ (0, import_react15.createElement)(DeliveryHeader, { view: payload, repo, onRetry, onProbe }), ++ (0, import_react15.createElement)( + "div", + { className: "bt-del-columns" }, -+ DELIVERY_COLUMNS.map((column) => (0, import_react14.createElement)( ++ DELIVERY_COLUMNS.map((column) => (0, import_react15.createElement)( + "section", + { key: column.id, className: "bt-del-column", "data-column": column.id }, -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)( + "h3", + { className: "bt-board-column-head" }, -+ (0, import_react14.createElement)("span", null, t(column.label)), -+ (0, import_react14.createElement)("span", { className: "bt-board-count" }, String((columns[column.id] ?? []).length)) ++ (0, import_react15.createElement)("span", null, t(column.label)), ++ (0, import_react15.createElement)("span", { className: "bt-board-count" }, String((columns[column.id] ?? []).length)) + ), -+ (columns[column.id] ?? []).length === 0 ? (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("Nothing here.")) : (columns[column.id] ?? []).map((card) => (0, import_react14.createElement)(DeliveryCard, { key: `${card.kind}-${card.number}`, card })) ++ (columns[column.id] ?? []).length === 0 ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("Nothing here.")) : (columns[column.id] ?? []).map((card) => (0, import_react15.createElement)(DeliveryCard, { key: `${card.kind}-${card.number}`, card })) + )) + ) + ); +} +function DeliveryView({ actions }) { + const [view, retry] = useDeliveryBoard(actions); -+ return (0, import_react14.createElement)(DeliveryBoard, { ++ return (0, import_react15.createElement)(DeliveryBoard, { + view, + repo: view?.repo ?? null, + onRetry: actions?.deliveryBoard ? retry : null, @@ -6830,27 +10482,41 @@ diff --git a/client.js b/client.js + }); +} +function Label({ name }) { -+ return (0, import_react14.createElement)("span", { className: "bt-del-label", "data-label": name }, name); ++ return (0, import_react15.createElement)("span", { className: "bt-del-label", "data-label": name }, name); +} +function DeliveryCard({ card }) { + const badge = badgeLabel(card?.badge); -+ return (0, import_react14.createElement)( ++ return (0, import_react15.createElement)( + "article", + { className: "bt-del-card", "data-kind": card?.kind, "data-number": card?.number, "data-badge": card?.badge ?? void 0 }, -+ (0, import_react14.createElement)( ++ (0, import_react15.createElement)( + "span", + { className: "bt-del-card-top" }, -+ (0, import_react14.createElement)("span", { className: "bt-del-id" }, `#${card?.number ?? "?"}`), -+ badge ? (0, import_react14.createElement)("span", { className: "bt-del-badge", "data-badge": card?.badge }, badge) : null ++ (0, import_react15.createElement)("span", { className: "bt-del-id" }, `#${card?.number ?? "?"}`), ++ badge ? (0, import_react15.createElement)("span", { className: "bt-del-badge", "data-badge": card?.badge }, badge) : null + ), -+ (0, import_react14.createElement)("span", { className: "bt-del-title" }, card?.title ?? ""), -+ card?.labels?.length ? (0, import_react14.createElement)("span", { className: "bt-del-labels" }, card.labels.map((name) => (0, import_react14.createElement)(Label, { key: name, name }))) : null, -+ card?.owner ? (0, import_react14.createElement)("span", { className: "bt-del-owner", "data-kind": card.ownerKind ?? "human" }, card.owner) : null, -+ card?.linked ? (0, import_react14.createElement)("span", { className: "bt-del-linked" }, t("linked to #{number}", { number: card.linked })) : null, -+ card?.sha ? (0, import_react14.createElement)("span", { className: "bt-del-sha" }, card.sha) : null, -+ card?.url ? (0, import_react14.createElement)("a", { className: "bt-del-sha", href: card.url, target: "_blank", rel: "noreferrer" }, t("open on GitHub")) : null -+ ); -+} ++ (0, import_react15.createElement)("span", { className: "bt-del-title" }, card?.title ?? ""), ++ card?.labels?.length ? (0, import_react15.createElement)("span", { className: "bt-del-labels" }, card.labels.map((name) => (0, import_react15.createElement)(Label, { key: name, name }))) : null, ++ card?.owner ? (0, import_react15.createElement)("span", { className: "bt-del-owner", "data-kind": card.ownerKind ?? "human" }, card.owner) : null, ++ card?.linked ? (0, import_react15.createElement)("span", { className: "bt-del-linked" }, t("linked to #{number}", { number: card.linked })) : null, ++ card?.sha ? (0, import_react15.createElement)("span", { className: "bt-del-sha" }, card.sha) : null, ++ card?.url ? (0, import_react15.createElement)("a", { className: "bt-del-sha", href: card.url, target: "_blank", rel: "noreferrer" }, t("open on GitHub")) : null + ); + } +-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +-function callTree(root) { +- const calls = []; +- const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; +- if (seen.has(id)) return; +- seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); +- }; +- visit(root); +- return calls; +var DELIVERY_CSS = ` +.bt-del-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px} +.bt-del-head{display:flex;flex-direction:column;gap:6px;border-bottom:.5px solid var(--bt-line);padding-bottom:8px} @@ -6893,24 +10559,101 @@ diff --git a/client.js b/client.js + .bt-del-columns{flex-direction:column} + .bt-del-column{min-width:0} +} -+`; ++` + DELIVERY_CHART_CSS; + +// src/client/kanban.js -+var import_react15 = require("react"); ++var import_react16 = require("react"); +var POLL_MS4 = 15e3; +function BoardIcon() { -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "svg", + { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, -+ (0, import_react15.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), -+ (0, import_react15.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), -+ (0, import_react15.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) ++ (0, import_react16.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }), ++ (0, import_react16.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }), ++ (0, import_react16.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) + ); -+} + } +-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 shortId(id) { + const hex = String(id ?? "").replace(/^task-/, ""); + return `#${hex.slice(-6)}`; -+} + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +- const self = roster.byId[sessionId]; +- switch (tool) { +- case "message_bot": { +- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "stop_bot": { +- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "create_bot": { +- const created = findBot(arg("name")); +- if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); +- } +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; +- } +- case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); +- case "create_group": +- case "update_group": +- case "delete_group": +- case "post_to_group": { +- const verbs = { +- create_group: ["Created group", "Couldn't create the group"], +- update_group: ["Updated group", "Couldn't update the group"], +- delete_group: ["Deleted group", "Couldn't delete the group"], +- post_to_group: ["Posted in", "Couldn't post in the group"] +- }[tool]; +- if (!done) return null; +- const result = contentText(call2.content); +- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); +- const name = named[2]; +- const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); +- } +- case "ask_user": { +- const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- } +- case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); +- case "ask_user_question": { +- if (!done) return null; +- let parsed = null; +- try { +- parsed = JSON.parse(contentText(call2.content)); +- } catch { +- parsed = null; +- } +- const answer = parsed?.answers?.[0]; +- const question = argValue("questions")?.[0]; +- if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- } +- case "read_group_chat": { +- if (!done) return null; +- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +function boardColumns(tasks) { + const columns = { "not-started": [], "in-progress": [], done: [] }; + for (const task of tasks ?? []) { @@ -6967,7 +10710,10 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); -+ } + } +- case "remember": +- case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; + }; + walk("", 0); + const groups = (roster?.rooms ?? []).filter((room2) => room2 && room2.hidden !== true && room2.origin !== "face-to-face" && adminOf(roster, room2)).map((room2) => ({ kind: "group", id: room2.id, name: room2.displayName || room2.name || t("Group"), room: room2 })); @@ -7001,10 +10747,45 @@ diff --git a/client.js b/client.js + return t("Cancelled"); + case "failed": + return t("Failed"); -+ default: -+ return t("Not started"); -+ } -+} + default: +- return null; ++ return t("Not started"); + } + } +-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)))); +-} +- +-// src/client/activity-row.js +-var import_react7 = require("react"); +-var import_react_dom2 = require("react-dom"); +-var PHRASE_HOLD_MS = 800; +-var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +-} +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); + if (column === "done") return t("Complete task"); @@ -7026,16 +10807,69 @@ diff --git a/client.js b/client.js + if (!map.has(id)) { + map.set(id, []); + order.push(id); -+ } + } +- 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; + map.get(id).push(task); + } + return order.map((id) => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })); -+} + } +-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 useBoardTasks(actions, scope) { -+ const [own, setOwn] = (0, import_react15.useState)([]); -+ const [groupTasks, setGroupTasks] = (0, import_react15.useState)([]); ++ const [own, setOwn] = (0, import_react16.useState)([]); ++ const [groupTasks, setGroupTasks] = (0, import_react16.useState)([]); + const key = scope?.kind === "group" ? `group:${scope.roomId}` : "own"; -+ (0, import_react15.useEffect)(() => { ++ (0, import_react16.useEffect)(() => { + let stopped = false; + const load = () => { + const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.()); @@ -7044,13 +10878,20 @@ diff --git a/client.js b/client.js + if (scope?.kind === "group") setGroupTasks(list); + else setOwn(list); + }).catch(() => void 0); -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); +- }); +- scan(); + load(); + const timer = setInterval(load, POLL_MS4); -+ 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) => { @@ -7059,9 +10900,83 @@ diff --git a/client.js b/client.js + else setOwn(write); + }; + return { own, tasks, 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)( +function RailButton({ row, selected, meta, onSelect }) { -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "button", + { + type: "button", @@ -7073,8 +10988,8 @@ diff --git a/client.js b/client.js + style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : void 0, + onClick: () => onSelect(row.kind === "group" ? { kind: "group", roomId: row.id } : { kind: "bot", botId: row.id }) + }, -+ (0, import_react15.createElement)("span", { className: "bt-board-rail-name" }, row.name), -+ meta ? (0, import_react15.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null ++ (0, import_react16.createElement)("span", { className: "bt-board-rail-name" }, row.name), ++ meta ? (0, import_react16.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null + ); +} +function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { @@ -7084,7 +10999,7 @@ diff --git a/client.js b/client.js + const who = botName(roster, task); + const question = task.waitingFor === "input" ? task.inputRequest?.question : ""; + const overall = task.kind === "group"; -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "article", + { + className: "bt-board-card", @@ -7100,35 +11015,35 @@ diff --git a/client.js b/client.js + }, + onDragEnd: () => onDrag?.(null) + }, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "span", + { className: "bt-board-card-top" }, -+ (0, import_react15.createElement)("span", { className: "bt-board-id" }, shortId(task.id)), -+ (0, import_react15.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)), -+ (0, import_react15.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status)) ++ (0, import_react16.createElement)("span", { className: "bt-board-id" }, shortId(task.id)), ++ (0, import_react16.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)), ++ (0, import_react16.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status)) + ), -+ (0, import_react15.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")), -+ overall ? (0, import_react15.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null, -+ partOf2 ? (0, import_react15.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null, -+ task.note ? (0, import_react15.createElement)("span", { className: "bt-board-note" }, task.note) : null, -+ question ? (0, import_react15.createElement)("span", { className: "bt-board-wait" }, question) : null, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")), ++ overall ? (0, import_react16.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null, ++ partOf2 ? (0, import_react16.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null, ++ task.note ? (0, import_react16.createElement)("span", { className: "bt-board-note" }, task.note) : null, ++ question ? (0, import_react16.createElement)("span", { className: "bt-board-wait" }, question) : null, ++ (0, import_react16.createElement)( + "span", + { className: "bt-board-foot" }, -+ who ? (0, import_react15.createElement)("span", { className: "bt-board-who" }, who) : null, -+ (0, import_react15.createElement)( ++ who ? (0, import_react16.createElement)("span", { className: "bt-board-who" }, who) : null, ++ (0, import_react16.createElement)( + "span", + { className: "bt-board-progress" }, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "span", + { className: "bt-board-progress-bar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progress, "aria-label": t("Progress") }, -+ (0, import_react15.createElement)("span", { style: { width: `${progress}%` } }) ++ (0, import_react16.createElement)("span", { style: { width: `${progress}%` } }) + ), -+ (0, import_react15.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress })) ++ (0, import_react16.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress })) + ) + ), -+ movable && column === "not-started" ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "in-progress") }, t("Start")) : null, -+ movable && column === "in-progress" && task.status !== "blocked" ? (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null ++ movable && column === "not-started" ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "in-progress") }, t("Start")) : null, ++ movable && column === "in-progress" && task.status !== "blocked" ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null + ); +} +function cardProps(task, roster, onMove, onDrag, partOf2) { @@ -7138,7 +11053,7 @@ diff --git a/client.js b/client.js + const tree = lanes ? columnTree(tasks) : null; + const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; + const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "section", + { + className: "bt-board-column", @@ -7155,31 +11070,31 @@ diff --git a/client.js b/client.js + onDrag?.(null); + } + }, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "h3", + { className: "bt-board-column-head" }, -+ (0, import_react15.createElement)("span", null, columnTitle(column)), -+ (0, import_react15.createElement)("span", { className: "bt-board-count" }, String(tasks.length)) ++ (0, import_react16.createElement)("span", null, columnTitle(column)), ++ (0, import_react16.createElement)("span", { className: "bt-board-count" }, String(tasks.length)) + ), -+ dragging ? (0, import_react15.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, -+ tasks.length === 0 && !dragging ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, -+ tree ? tree.nests.map((nest) => (0, import_react15.createElement)( ++ dragging ? (0, import_react16.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, ++ tasks.length === 0 && !dragging ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, ++ tree ? tree.nests.map((nest) => (0, import_react16.createElement)( + "div", + { key: nest.task.id, className: "bt-board-nest" }, -+ (0, import_react15.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), -+ nest.children.map((child) => (0, import_react15.createElement)( ++ (0, import_react16.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)), ++ nest.children.map((child) => (0, import_react16.createElement)( + "div", + { key: child.id, className: "bt-board-subtask", "data-parent": nest.task.id }, -+ (0, import_react15.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag)) ++ (0, import_react16.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag)) + )) + )) : null, -+ linked.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })), -+ lanes ? lanesOf(plain, roster).map((lane) => (0, import_react15.createElement)( ++ linked.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })), ++ lanes ? lanesOf(plain, roster).map((lane) => (0, import_react16.createElement)( + "div", + { key: lane.id, className: "bt-board-lane" }, -+ (0, import_react15.createElement)("span", { className: "bt-board-lane-name" }, lane.name), -+ lane.tasks.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) -+ )) : plain.map((task) => (0, import_react15.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) ++ (0, import_react16.createElement)("span", { className: "bt-board-lane-name" }, lane.name), ++ lane.tasks.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) ++ )) : plain.map((task) => (0, import_react16.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) + ); +} +function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { @@ -7187,16 +11102,26 @@ diff --git a/client.js b/client.js + const lanes = scope?.kind === "group"; + const selectedBot = scope?.kind === "bot" ? scope.botId : ""; + const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; -+ return (0, import_react15.createElement)( -+ "div", ++ return (0, import_react16.createElement)( + "div", +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( +- "span", +- { className: "bt-typing-text" }, +- (0, import_react7.createElement)( +- "span", +- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null + { className: "bt-board-body" }, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "aside", + { className: "bt-board-rail", "aria-label": t("Task board") }, -+ (0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")), -+ rail.bots.length === 0 ? (0, import_react15.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => { ++ (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")), ++ rail.bots.length === 0 ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => { + const count = counts2?.[row.id] ?? { done: 0, total: 0 }; -+ return (0, import_react15.createElement)(RailButton, { ++ return (0, import_react16.createElement)(RailButton, { + key: row.id, + row, + selected: row.id === selectedBot, @@ -7204,27 +11129,27 @@ diff --git a/client.js b/client.js + meta: count.total > 0 ? t("{done}/{total}", { done: count.done, total: count.total }) : "" + }); + }), -+ rail.groups.length > 0 ? (0, import_react15.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, -+ rail.groups.map((row) => (0, import_react15.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) ++ rail.groups.length > 0 ? (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, ++ rail.groups.map((row) => (0, import_react16.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) + ), -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "div", + { className: "bt-board-main" }, -+ error ? (0, import_react15.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, -+ lanes ? (0, import_react15.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, -+ scope?.kind === "bot" && onCreate ? (0, import_react15.createElement)( ++ error ? (0, import_react16.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, ++ lanes ? (0, import_react16.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, ++ scope?.kind === "bot" && onCreate ? (0, import_react16.createElement)( + "form", + { className: "bt-board-add", onSubmit: (event) => { + event.preventDefault(); + onCreate(draft); + } }, -+ (0, import_react15.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }), -+ (0, import_react15.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task")) ++ (0, import_react16.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }), ++ (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task")) + ) : null, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "div", + { className: "bt-board-columns" }, -+ ["not-started", "in-progress", "done"].map((column) => (0, import_react15.createElement)(BoardColumn, { ++ ["not-started", "in-progress", "done"].map((column) => (0, import_react16.createElement)(BoardColumn, { + key: column, + column, + tasks: columns[column], @@ -7234,14 +11159,24 @@ diff --git a/client.js b/client.js + onMove, + onDrag + })) -+ ) -+ ) + ) + ) +- ), host); + ); -+} + } +- +-// src/client/dsh-offer.js +-var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +-var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; +-var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); +-function showsDshRow(dsh, app) { +- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; +- if (dsh.kind === "npm") return true; +- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); +function KanbanRow({ wide, useUi, actions }) { + const open = useUi ? useUi((value) => Boolean(value.board)) : false; + const label = t("Tasks"); -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "button", + { + type: "button", @@ -7252,8 +11187,8 @@ diff --git a/client.js b/client.js + "aria-expanded": open, + onClick: () => actions.openBoard() + }, -+ (0, import_react15.createElement)("span", { className: "bt-board-mark" }, (0, import_react15.createElement)(BoardIcon)), -+ wide ? (0, import_react15.createElement)("span", { className: "bt-board-label" }, label) : null ++ (0, import_react16.createElement)("span", { className: "bt-board-mark" }, (0, import_react16.createElement)(BoardIcon)), ++ wide ? (0, import_react16.createElement)("span", { className: "bt-board-label" }, label) : null + ); +} +function scopeName(roster, scope) { @@ -7262,14 +11197,14 @@ diff --git a/client.js b/client.js + return ""; +} +function KanbanDialog({ roster, actions, leaving = false }) { -+ const [view, setView] = (0, import_react15.useState)("tasks"); ++ const [view, setView] = (0, import_react16.useState)("tasks"); + const rail = boardRail(roster); -+ const [scope, setScope] = (0, import_react15.useState)(() => rail.bots[0] ? { kind: "bot", botId: rail.bots[0].id } : rail.groups[0] ? { kind: "group", roomId: rail.groups[0].id } : null); -+ const [dragging, setDragging] = (0, import_react15.useState)(null); -+ const [error, setError] = (0, import_react15.useState)(""); -+ const [draft, setDraft] = (0, import_react15.useState)(""); ++ const [scope, setScope] = (0, import_react16.useState)(() => rail.bots[0] ? { kind: "bot", botId: rail.bots[0].id } : rail.groups[0] ? { kind: "group", roomId: rail.groups[0].id } : null); ++ const [dragging, setDragging] = (0, import_react16.useState)(null); ++ const [error, setError] = (0, import_react16.useState)(""); ++ const [draft, setDraft] = (0, import_react16.useState)(""); + const { own, tasks, replace } = useBoardTasks(actions, scope); -+ const close = (0, import_react15.useCallback)(() => actions.closeBoard(), []); ++ const close = (0, import_react16.useCallback)(() => actions.closeBoard(), []); + useEscape(close); + const counts2 = {}; + for (const task of own) { @@ -7301,28 +11236,28 @@ diff --git a/client.js b/client.js + } + }; + const name = scopeName(roster, scope); -+ return (0, import_react15.createElement)( ++ return (0, import_react16.createElement)( + "div", + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)("div", { className: "bt-scrim", onClick: close }), ++ (0, import_react16.createElement)( + "div", + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)( + "div", + { className: "bt-mem-head" }, -+ (0, import_react15.createElement)("h2", { id: "bt-board-heading" }, view === "delivery" ? t("Delivery") : t("Tasks")), -+ (0, import_react15.createElement)( ++ (0, import_react16.createElement)("h2", { id: "bt-board-heading" }, view === "delivery" ? t("Delivery") : t("Tasks")), ++ (0, import_react16.createElement)( + "div", + { className: "bt-board-views", role: "tablist", "aria-label": t("Board view") }, -+ (0, import_react15.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "tasks", "aria-selected": view === "tasks" ? "true" : "false", onClick: () => setView("tasks") }, t("Tasks")), -+ (0, import_react15.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "delivery", "aria-selected": view === "delivery" ? "true" : "false", onClick: () => setView("delivery") }, t("Delivery")) ++ (0, import_react16.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "tasks", "aria-selected": view === "tasks" ? "true" : "false", onClick: () => setView("tasks") }, t("Tasks")), ++ (0, import_react16.createElement)("button", { type: "button", role: "tab", className: "bt-soft bt-soft-sm", "data-view": "delivery", "aria-selected": view === "delivery" ? "true" : "false", onClick: () => setView("delivery") }, t("Delivery")) + ), -+ view === "tasks" ? (0, import_react15.createElement)("span", { className: "bt-mem-status" }, name) : null, -+ (0, import_react15.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react15.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react15.createElement)(CloseIcon)) ++ view === "tasks" ? (0, import_react16.createElement)("span", { className: "bt-mem-status" }, name) : null, ++ (0, import_react16.createElement)("span", { className: "bt-mem-head-gap" }), ++ (0, import_react16.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react16.createElement)(CloseIcon)) + ), -+ view === "delivery" ? (0, import_react15.createElement)(DeliveryView, { actions }) : (0, import_react15.createElement)(BoardView, { ++ view === "delivery" ? (0, import_react16.createElement)(DeliveryView, { actions }) : (0, import_react16.createElement)(BoardView, { + roster, + tasks, + rail, @@ -7339,7 +11274,7 @@ diff --git a/client.js b/client.js + }) + ) + ); -+} + } +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-views{display:flex;gap:6px;align-items:center} @@ -7405,20 +11340,20 @@ diff --git a/client.js b/client.js + .bt-board-column{min-width:0} +} +` + DELIVERY_CSS; -+ + // src/client/sidebar.js -var import_react8 = require("react"); -+var import_react16 = require("react"); ++var import_react17 = require("react"); var import_react_dom3 = require("react-dom"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); function useMarkedUnread(id, active, marked, actions) { - (0, import_react8.useEffect)(() => { -+ (0, import_react16.useEffect)(() => { ++ (0, import_react17.useEffect)(() => { if (active && marked) actions.clearUnread(id); }, [active, marked]); } -var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); -+var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.createElement)(PinMarkIcon)); ++var PinMark = () => (0, import_react17.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react17.createElement)(PinMarkIcon)); var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { @@ -7427,7 +11362,7 @@ diff --git a/client.js b/client.js }); -var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); -function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { -+var workLine = (lead, tool, more) => (0, import_react16.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); ++var workLine = (lead, tool, more) => (0, import_react17.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); +function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id; const part = currentPart(bot); @@ -7465,7 +11400,7 @@ diff --git a/client.js b/client.js + const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); if (tile && wide) { - return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( -+ return (0, import_react16.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react16.createElement)( ++ return (0, import_react17.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react17.createElement)( "button", { type: "button", @@ -7476,16 +11411,16 @@ diff --git a/client.js b/client.js - (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), - (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), - bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null -+ (0, import_react16.createElement)(BotAvatar, { bot, size: 60, state, main: true }), -+ (0, import_react16.createElement)("span", { className: "bt-tile-name" }, shown), ++ (0, import_react17.createElement)(BotAvatar, { bot, size: 60, state, main: true }), ++ (0, import_react17.createElement)("span", { className: "bt-tile-name" }, shown), + // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. -+ unread && !waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, -+ bot.role ? (0, import_react16.createElement)("span", { className: "bt-chip" }, bot.role) : null ++ unread && !waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null, ++ bot.role ? (0, import_react17.createElement)("span", { className: "bt-chip" }, bot.role) : null )); } - return (0, import_react8.createElement)( -+ const unreadMark = wide && waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; -+ const row = (0, import_react16.createElement)( ++ const unreadMark = wide && waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null; ++ const row = (0, import_react17.createElement)( "button", { type: "button", @@ -7502,27 +11437,27 @@ diff --git a/client.js b/client.js - (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), - pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, - wide ? (0, import_react8.createElement)( -+ (0, import_react16.createElement)(BotAvatar, { bot, size: 36, state, main }), -+ pinned && !wide ? (0, import_react16.createElement)(PinMark) : null, -+ wide ? (0, import_react16.createElement)( ++ (0, import_react17.createElement)(BotAvatar, { bot, size: 36, state, main }), ++ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null, ++ wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), - awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( + "span", + { className: "bt-row-name" }, -+ (0, import_react16.createElement)("span", { className: "bt-row-title" }, shown), -+ bot.remote ? (0, import_react16.createElement)(RemoteMark, { remote: bot.remote }) : null, -+ pinned ? (0, import_react16.createElement)(PinMark) : null ++ (0, import_react17.createElement)("span", { className: "bt-row-title" }, shown), ++ bot.remote ? (0, import_react17.createElement)(RemoteMark, { remote: bot.remote }) : null, ++ pinned ? (0, import_react17.createElement)(PinMark) : null + ), -+ secondLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ++ secondLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ) : null, - wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + unreadMark + ); + const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); -+ const settings = wide ? (0, import_react16.createElement)("button", { ++ const settings = wide ? (0, import_react17.createElement)("button", { + type: "button", + className: "bt-row-settings", + "aria-label": settingsLabel, @@ -7533,8 +11468,8 @@ diff --git a/client.js b/client.js + keepSidebarStill(event.currentTarget); + actions.openSettings("bots", id); + } -+ }, (0, import_react16.createElement)(GearIcon)) : null; -+ if (settings) return (0, import_react16.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); ++ }, (0, import_react17.createElement)(GearIcon)) : null; ++ if (settings) return (0, import_react17.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); + return row; +} +function keepSidebarStill(node) { @@ -7547,14 +11482,14 @@ diff --git a/client.js b/client.js + if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); +} +function RemoteMark({ remote }) { -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( + "span", + { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( + "svg", + { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, -+ (0, import_react16.createElement)("circle", { cx: 12, cy: 12, r: 9 }), -+ (0, import_react16.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) ++ (0, import_react17.createElement)("circle", { cx: 12, cy: 12, r: 9 }), ++ (0, import_react17.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ) + ); +} @@ -7566,15 +11501,15 @@ diff --git a/client.js b/client.js + return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); +} +function FaceMark() { -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( + "span", + { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( + "svg", + { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, -+ (0, import_react16.createElement)("circle", { cx: 9, cy: 8, r: 3 }), -+ (0, import_react16.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), -+ (0, import_react16.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) ++ (0, import_react17.createElement)("circle", { cx: 9, cy: 8, r: 3 }), ++ (0, import_react17.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }), ++ (0, import_react17.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" }) + ) ); } @@ -7603,7 +11538,7 @@ diff --git a/client.js b/client.js + const alert = question || waiting; + const face = room2.origin === "face-to-face"; + const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", "); -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "button", { type: "button", @@ -7623,25 +11558,25 @@ diff --git a/client.js b/client.js - (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), - pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, - wide ? (0, import_react8.createElement)( -+ (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), -+ pinned && !wide ? (0, import_react16.createElement)(PinMark) : null, -+ wide ? (0, import_react16.createElement)( ++ (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), ++ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null, ++ wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), - workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react16.createElement)("span", { className: "bt-row-name" }, (0, import_react16.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react16.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react16.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react16.createElement)(PinMark) : null), -+ workingLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null ++ (0, import_react17.createElement)("span", { className: "bt-row-name" }, (0, import_react17.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react17.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react17.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react17.createElement)(PinMark) : null), ++ workingLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null ) : null, - wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ wide && alert && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ wide && alert && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ); } function AgentSessionRow({ session, agent, actions, renaming, index }) { - const input = (0, import_react8.useRef)(null); - (0, import_react8.useLayoutEffect)(() => { -+ const input = (0, import_react16.useRef)(null); -+ (0, import_react16.useLayoutEffect)(() => { ++ const input = (0, import_react17.useRef)(null); ++ (0, import_react17.useLayoutEffect)(() => { if (!renaming) return; input.current?.focus(); input.current?.select(); @@ -7650,11 +11585,11 @@ diff --git a/client.js b/client.js })(); }; - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "div", { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, - (0, import_react8.createElement)("input", { -+ (0, import_react16.createElement)("input", { ++ (0, import_react17.createElement)("input", { ref: input, defaultValue: session.title, "aria-label": t("Rename"), @@ -7663,7 +11598,7 @@ diff --git a/client.js b/client.js ); } - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "button", { type: "button", @@ -7673,8 +11608,8 @@ diff --git a/client.js b/client.js }, - (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), - (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") -+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, session.title), -+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, session.title), ++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { @@ -7683,11 +11618,11 @@ diff --git a/client.js b/client.js const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "div", { className: "bt-agent" }, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "button", { type: "button", @@ -7697,39 +11632,39 @@ diff --git a/client.js b/client.js }, - (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), - wide ? (0, import_react8.createElement)( -+ (0, import_react16.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react16.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), -+ wide ? (0, import_react16.createElement)( ++ (0, import_react17.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react17.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), ++ wide ? (0, import_react17.createElement)( "span", { className: "bt-row-body" }, - (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), - preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react16.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), -+ preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null ++ (0, import_react17.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), ++ preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null ) : null, - wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ++ wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null ), - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "div", { className: "bt-agent-sub", "data-open": open || void 0 }, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "div", { className: "bt-agent-sub-in" }, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "button", { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); } }, - (0, import_react8.createElement)(PlusIcon), - (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) -+ (0, import_react16.createElement)(PlusIcon), -+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) ++ (0, import_react17.createElement)(PlusIcon), ++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) ), - sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { -+ sessions.map((session, index) => (0, import_react16.createElement)(AgentSessionRow, { ++ sessions.map((session, index) => (0, import_react17.createElement)(AgentSessionRow, { key: session.id, session, agent, @@ -7739,8 +11674,8 @@ diff --git a/client.js b/client.js function useListGlide(listRef, layout, key) { - const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); - (0, import_react8.useLayoutEffect)(() => { -+ const tops = (0, import_react16.useRef)({ layout, map: /* @__PURE__ */ new Map() }); -+ (0, import_react16.useLayoutEffect)(() => { ++ const tops = (0, import_react17.useRef)({ layout, map: /* @__PURE__ */ new Map() }); ++ (0, import_react17.useLayoutEffect)(() => { const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); @@ -7748,7 +11683,7 @@ diff --git a/client.js b/client.js tops.current = { layout, map: next }; }, [layout, key]); } -+var NodeActions = class extends import_react16.Component { ++var NodeActions = class extends import_react17.Component { + constructor(props) { + super(props); + this.state = { failed: false }; @@ -7760,33 +11695,33 @@ diff --git a/client.js b/client.js + console.error("flybot: nodeActions failed", error); + } + render() { -+ return this.state.failed ? null : (0, import_react16.createElement)(this.props.component, { bot: this.props.bot }); ++ return this.state.failed ? null : (0, import_react17.createElement)(this.props.component, { bot: this.props.bot }); + } +}; +function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) { + const { bot, depth, children } = node; + const closed = collapsed[bot.id] === true; -+ return (0, import_react16.createElement)( -+ import_react16.Fragment, ++ return (0, import_react17.createElement)( ++ import_react17.Fragment, + null, -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( + "div", + { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } }, -+ children.length ? (0, import_react16.createElement)( ++ children.length ? (0, import_react17.createElement)( + "button", + { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) }, -+ (0, import_react16.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") -+ ) : (0, import_react16.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), -+ (0, import_react16.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), -+ typeof actions === "function" ? (0, import_react16.createElement)(NodeActions, { component: actions, bot }) : null ++ (0, import_react17.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾") ++ ) : (0, import_react17.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }), ++ (0, import_react17.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }), ++ typeof actions === "function" ? (0, import_react17.createElement)(NodeActions, { component: actions, bot }) : null + ), -+ closed ? null : children.map((child) => (0, import_react16.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) ++ closed ? null : children.map((child) => (0, import_react17.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) + ); +} function TeamSidebar(props) { const { wide, useRoster, useUi, useSessions, actions } = props; - const listRef = (0, import_react8.useRef)(null); -+ const listRef = (0, import_react16.useRef)(null); ++ const listRef = (0, import_react17.useRef)(null); const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); @@ -7795,7 +11730,7 @@ diff --git a/client.js b/client.js return JSON.stringify(stamps); }); - const order = (0, import_react8.useMemo)(() => { -+ const order = (0, import_react16.useMemo)(() => { ++ const order = (0, import_react17.useMemo)(() => { const stamps = JSON.parse(updated); const entries2 = [ - ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), @@ -7813,7 +11748,7 @@ diff --git a/client.js b/client.js }, [roster, updated]); const main = roster.byId[roster.mainBotId]; + const structure = useSidebarStructure(); -+ const [collapsed, setCollapsed] = (0, import_react16.useState)({}); ++ const [collapsed, setCollapsed] = (0, import_react17.useState)({}); + const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); + const inTree = (entry) => entry.kind === "bot" && !entry.item.remote; + const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false; @@ -7826,11 +11761,11 @@ diff --git a/client.js b/client.js actions.openPalette(event.key); }; - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "div", { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, - wide ? (0, import_react8.createElement)( -+ wide ? (0, import_react16.createElement)( ++ wide ? (0, import_react17.createElement)( "div", { className: "bt-side-top" }, - (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), @@ -7839,31 +11774,31 @@ diff --git a/client.js b/client.js - (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) - ) : (0, import_react8.createElement)( - import_react8.Fragment, -+ (0, import_react16.createElement)("span", { className: "bt-brand" }, (0, import_react16.createElement)(BrandMark, { size: 22 }), (0, import_react16.createElement)(BrandName)), -+ (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react16.createElement)(SearchIcon)), -+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon)) -+ ) : (0, import_react16.createElement)( -+ import_react16.Fragment, ++ (0, import_react17.createElement)("span", { className: "bt-brand" }, (0, import_react17.createElement)(BrandMark, { size: 22 }), (0, import_react17.createElement)(BrandName)), ++ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), ++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react17.createElement)(SearchIcon)), ++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon)) ++ ) : (0, import_react17.createElement)( ++ import_react17.Fragment, null, - (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), - (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -+ (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon)) ++ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), ++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon)) ), - wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, - (0, import_react8.createElement)( -+ wide && roster.readOnly ? (0, import_react16.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, -+ (0, import_react16.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), -+ (0, import_react16.createElement)(KanbanRow, { wide, useUi, actions }), -+ (0, import_react16.createElement)( ++ wide && roster.readOnly ? (0, import_react17.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, ++ (0, import_react17.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), ++ (0, import_react17.createElement)(KanbanRow, { wide, useUi, actions }), ++ (0, import_react17.createElement)( "div", { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, - main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), - order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) -+ tree ? (0, import_react16.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react16.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react16.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react16.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), -+ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react16.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react16.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react16.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), -+ wide ? (0, import_react16.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react16.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null ++ tree ? (0, import_react17.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react17.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react17.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react17.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), ++ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react17.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react17.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react17.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), ++ wide ? (0, import_react17.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react17.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null ) ); } @@ -7875,12 +11810,12 @@ diff --git a/client.js b/client.js - const [dshError, setDshError] = (0, import_react8.useState)(""); - const button = (0, import_react8.useRef)(null); - (0, import_react8.useEffect)(() => { -+ const [open, setOpen] = (0, import_react16.useState)(false); -+ const [box, setBox] = (0, import_react16.useState)(null); -+ const [error, setError] = (0, import_react16.useState)(""); -+ const [dshError, setDshError] = (0, import_react16.useState)(""); -+ const button = (0, import_react16.useRef)(null); -+ (0, import_react16.useEffect)(() => { ++ const [open, setOpen] = (0, import_react17.useState)(false); ++ const [box, setBox] = (0, import_react17.useState)(null); ++ const [error, setError] = (0, import_react17.useState)(""); ++ const [dshError, setDshError] = (0, import_react17.useState)(""); ++ const button = (0, import_react17.useRef)(null); ++ (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; @@ -7896,11 +11831,11 @@ diff --git a/client.js b/client.js - return (0, import_react8.createElement)( - import_react8.Fragment, + const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); -+ return (0, import_react16.createElement)( -+ import_react16.Fragment, ++ return (0, import_react17.createElement)( ++ import_react17.Fragment, null, - (0, import_react8.createElement)("button", { -+ (0, import_react16.createElement)("button", { ++ (0, import_react17.createElement)("button", { ref: button, type: "button", className: "bt-round bt-update", @@ -7910,30 +11845,30 @@ diff --git a/client.js b/client.js onClick: toggle - }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), - open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -+ }, (0, import_react16.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react16.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -+ open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)( ++ }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), ++ open && box ? (0, import_react_dom3.createPortal)((0, import_react17.createElement)( "div", - { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, - update?.available || phase !== "idle" ? (0, import_react8.createElement)( - import_react8.Fragment, + { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box }, -+ update?.available || phase !== "idle" ? (0, import_react16.createElement)( -+ import_react16.Fragment, ++ update?.available || phase !== "idle" ? (0, import_react17.createElement)( ++ import_react17.Fragment, null, - (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), - (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), - message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), -+ (0, import_react16.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), -+ message ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })), ++ (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), ++ message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, ++ (0, import_react17.createElement)( "div", { className: "bt-update-actions" }, - update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, - phase === "installed" ? null : (0, import_react8.createElement)("button", { -+ update.notesUrl ? (0, import_react16.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -+ phase === "installed" ? null : (0, import_react16.createElement)("button", { ++ update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, ++ phase === "installed" ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", disabled: phase === "installing", @@ -7942,23 +11877,23 @@ diff --git a/client.js b/client.js ) ) : null, - dshRow ? (0, import_react8.createElement)( -+ dshRow ? (0, import_react16.createElement)( ++ dshRow ? (0, import_react17.createElement)( "div", { className: "bt-update-dsh" }, - (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), - (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), - dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -+ (0, import_react16.createElement)("div", { className: "bt-update-text" }, dshText), -+ dshMessage ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), ++ (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText), ++ dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, ++ (0, import_react17.createElement)( "div", { className: "bt-update-actions" }, - dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { - }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { -+ dsh.kind === "app" ? dsh.available ? (0, import_react16.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { -+ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react16.createElement)("button", { ++ dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { ++ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", disabled: dshPhase === "installing", @@ -7990,11 +11925,11 @@ diff --git a/client.js b/client.js - setTip(false); - }; - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "span", { className: "bt-connect-wrap" }, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "button", - { - ref: button, @@ -8017,20 +11952,20 @@ diff --git a/client.js b/client.js - t("Plugin compatibility is not fully verified.") - ), document.body) : null + { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() }, -+ (0, import_react16.createElement)(PlugIcon), ++ (0, import_react17.createElement)(PlugIcon), + t("Connect apps") + ) ); } function YouAvatar() { - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "span", { className: "bt-you-wrap" }, - (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), - (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) -+ (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -+ (0, import_react16.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), ++ (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ); } var openShellSettings = null; @@ -8041,7 +11976,7 @@ diff --git a/client.js b/client.js +var MODELS_STEP_ID = "flybot-models"; +var MODELS_STEP_ORDER = 1e3; +function ModelsSettingsStep({ explicit, complete, openSection }) { -+ (0, import_react16.useLayoutEffect)(() => { ++ (0, import_react17.useLayoutEffect)(() => { + if (explicit) openSection("models"); + complete(); + }, []); @@ -8059,8 +11994,8 @@ diff --git a/client.js b/client.js function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { - const [open, setOpen] = (0, import_react8.useState)(false); - (0, import_react8.useLayoutEffect)(() => { -+ const [open, setOpen] = (0, import_react16.useState)(false); -+ (0, import_react16.useLayoutEffect)(() => { ++ const [open, setOpen] = (0, import_react17.useState)(false); ++ (0, import_react17.useLayoutEffect)(() => { if (!seat) return void 0; actions.seatAccount(true); return () => actions.seatAccount(false); @@ -8068,13 +12003,13 @@ diff --git a/client.js b/client.js - const [box, setBox] = (0, import_react8.useState)(null); - const button = (0, import_react8.useRef)(null); - (0, import_react8.useEffect)(() => { -+ const [box, setBox] = (0, import_react16.useState)(null); -+ const button = (0, import_react16.useRef)(null); -+ (0, import_react16.useEffect)(() => { ++ const [box, setBox] = (0, import_react17.useState)(null); ++ const button = (0, import_react17.useRef)(null); ++ (0, import_react17.useEffect)(() => { if (settingsOpen) setOpen(false); }, [settingsOpen]); - (0, import_react8.useEffect)(() => { -+ (0, import_react16.useEffect)(() => { ++ (0, import_react17.useEffect)(() => { if (!openSettings) return void 0; openShellSettings = openSettings; return () => { @@ -8082,7 +12017,7 @@ diff --git a/client.js b/client.js }; }, [openSettings]); - (0, import_react8.useEffect)(() => { -+ (0, import_react16.useEffect)(() => { ++ (0, import_react17.useEffect)(() => { if (!openOnboarding) return void 0; openShellOnboarding = openOnboarding; return () => { @@ -8090,7 +12025,7 @@ diff --git a/client.js b/client.js }; }, [openOnboarding]); - (0, import_react8.useEffect)(() => { -+ (0, import_react16.useEffect)(() => { ++ (0, import_react17.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; @@ -8112,30 +12047,30 @@ diff --git a/client.js b/client.js ]; - return (0, import_react8.createElement)( - import_react8.Fragment, -+ return (0, import_react16.createElement)( -+ import_react16.Fragment, ++ return (0, import_react17.createElement)( ++ import_react17.Fragment, null, - (0, import_react8.createElement)( -+ (0, import_react16.createElement)( ++ (0, import_react17.createElement)( "button", { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, - (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) -+ (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ), - open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -+ open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)( ++ open && box ? (0, import_react_dom3.createPortal)((0, import_react17.createElement)( "div", { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, - groups.map((group, index) => (0, import_react8.createElement)( - import_react8.Fragment, -+ (0, import_react16.createElement)(FlyBotAccountHeader, { actions }), -+ groups.map((group, index) => (0, import_react16.createElement)( -+ import_react16.Fragment, ++ (0, import_react17.createElement)(FlyBotAccountHeader, { actions }), ++ groups.map((group, index) => (0, import_react17.createElement)( ++ import_react17.Fragment, { key: index }, - index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { -+ index > 0 ? (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, hint]) => (0, import_react16.createElement)("button", { ++ index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", { key: label, type: "button", role: "menuitem", @@ -8144,7 +12079,7 @@ diff --git a/client.js b/client.js void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } - }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) -+ }, (0, import_react16.createElement)(Icon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) ++ }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) )) ), document.body) : null ); @@ -8154,8 +12089,8 @@ diff --git a/client.js b/client.js }); - const lastBot = (0, import_react8.useRef)(null); - (0, import_react8.useEffect)(() => { -+ const lastBot = (0, import_react16.useRef)(null); -+ (0, import_react16.useEffect)(() => { ++ const lastBot = (0, import_react17.useRef)(null); ++ (0, import_react17.useEffect)(() => { if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; @@ -8172,20 +12107,20 @@ diff --git a/client.js b/client.js const seated = props.useUi((value) => value.accountSeated === true); if (seated) return null; - return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); -+ return (0, import_react16.createElement)("span", { className: "bt-foot-account" }, (0, import_react16.createElement)(AccountLauncher, { ...props, seat: false })); ++ return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false })); } function BotReturn({ wide, useSurface, actions }) { const mode = useSurface((value) => value.mode); if (mode !== "agent") return null; - return (0, import_react8.createElement)( -+ return (0, import_react16.createElement)( ++ return (0, import_react17.createElement)( "button", - { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, - (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), - wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null + { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, -+ (0, import_react16.createElement)(FlyBotMark, { size: 20 }), -+ wide ? (0, import_react16.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null ++ (0, import_react17.createElement)(FlyBotMark, { size: 20 }), ++ wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null ); } var RETURN_CSS = ` @@ -8202,11 +12137,11 @@ diff --git a/client.js b/client.js `; function BrandMark({ size = 24 }) { - return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); -+ return (0, import_react16.createElement)("span", { className: "bt-brand-mark" }, (0, import_react16.createElement)(FlyBotMark, { size })); ++ return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size })); } function BrandName() { - return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); -+ return (0, import_react16.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); ++ return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot")); } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} @@ -8228,18 +12163,18 @@ diff --git a/client.js b/client.js // src/client/header.js -var import_react9 = require("react"); -+var import_react17 = require("react"); ++var import_react18 = require("react"); var import_react_dom4 = require("react-dom"); -var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); -+var GoArrow = () => (0, import_react17.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react17.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); ++var GoArrow = () => (0, import_react18.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react18.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); var MEMORY_NOTICE_MS = 3200; var noNews = (select) => select({}); function useMemoryNotice(ids, useMemoryNews) { const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); - const [shown, setShown] = (0, import_react9.useState)(false); - (0, import_react9.useEffect)(() => { -+ const [shown, setShown] = (0, import_react17.useState)(false); -+ (0, import_react17.useEffect)(() => { ++ const [shown, setShown] = (0, import_react18.useState)(false); ++ (0, import_react18.useEffect)(() => { const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; @@ -8249,19 +12184,19 @@ diff --git a/client.js b/client.js function MemoryNotice({ shown }) { - const inner = (0, import_react9.useRef)(null); - const [width, setWidth] = (0, import_react9.useState)(0); -+ const inner = (0, import_react17.useRef)(null); -+ const [width, setWidth] = (0, import_react17.useState)(0); ++ const inner = (0, import_react18.useRef)(null); ++ const [width, setWidth] = (0, import_react18.useState)(0); const label = t("Memory updated"); - (0, import_react9.useLayoutEffect)(() => { -+ (0, import_react17.useLayoutEffect)(() => { ++ (0, import_react18.useLayoutEffect)(() => { if (shown) setWidth(inner.current?.scrollWidth ?? 0); }, [label, shown]); - return (0, import_react9.createElement)( -+ return (0, import_react17.createElement)( ++ return (0, import_react18.createElement)( "span", { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, - (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) -+ (0, import_react17.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react17.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react17.createElement)(MemoryIcon)), label) ++ (0, import_react18.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react18.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react18.createElement)(MemoryIcon)), label) ); } function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { @@ -8270,7 +12205,7 @@ diff --git a/client.js b/client.js return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); - (0, import_react9.useEffect)(() => { -+ (0, import_react17.useEffect)(() => { ++ (0, import_react18.useEffect)(() => { if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); @@ -8304,11 +12239,11 @@ diff --git a/client.js b/client.js - return (0, import_react9.createElement)( + const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); + const memoryNotice = notice && changed2 !== null && roster.memory !== false; -+ return (0, import_react17.createElement)( ++ return (0, import_react18.createElement)( "div", { className: "bt-head" }, - (0, import_react9.createElement)( -+ (0, import_react17.createElement)( ++ (0, import_react18.createElement)( "button", { type: "button", @@ -8324,24 +12259,24 @@ 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)) -+ bot ? (0, import_react17.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -+ (0, import_react17.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_react17.createElement)(MemoryNotice, { shown: memoryNotice }), -+ (0, import_react17.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react17.createElement)(GoArrow)) ++ 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_react17.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -+ (0, import_react17.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), -+ (0, import_react17.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 }) ); } function useTranscriptView() { - const [view, setView] = (0, import_react9.useState)(null); - (0, import_react9.useEffect)(() => { -+ const [view, setView] = (0, import_react17.useState)(null); -+ (0, import_react17.useEffect)(() => { ++ const [view, setView] = (0, import_react18.useState)(null); ++ (0, import_react18.useEffect)(() => { let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); @@ -8361,24 +12296,24 @@ diff --git a/client.js b/client.js - const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); - const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false }); - (0, import_react9.useEffect)(() => { -+ const [release, setRelease] = (0, import_react17.useState)({ base: null, dismissed: false }); -+ const [above, setAbove] = (0, import_react17.useState)({ seen: false, dismissed: false }); -+ (0, import_react17.useEffect)(() => { ++ const [release, setRelease] = (0, import_react18.useState)({ base: null, dismissed: false }); ++ const [above, setAbove] = (0, import_react18.useState)({ seen: false, dismissed: false }); ++ (0, import_react18.useEffect)(() => { setRelease({ base: unpinned ? replies : null, dismissed: false }); }, [unpinned]); - (0, import_react9.useEffect)(() => { -+ (0, import_react17.useEffect)(() => { ++ (0, import_react18.useEffect)(() => { if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); }, [view?.divider]); + const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); -+ (0, import_react17.useEffect)(() => { ++ (0, import_react18.useEffect)(() => { + if (above.seen && movedOn) actions?.settleVisit?.(sessionId); + }, [above.seen, movedOn, sessionId]); const fresh = release.base === null ? 0 : replies - release.base; const showDown = unpinned && fresh > 0 && !release.dismissed; const showUp = view?.divider === "above" && !above.seen && !above.dismissed; - (0, import_react9.useEffect)(() => { -+ (0, import_react17.useEffect)(() => { ++ (0, import_react18.useEffect)(() => { const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); @@ -8387,19 +12322,19 @@ diff --git a/client.js b/client.js if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); - const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( -+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react17.createElement)( ++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react18.createElement)( "div", { className: "bt-news", "data-direction": direction, style }, - (0, import_react9.createElement)( -+ (0, import_react17.createElement)( ++ (0, import_react18.createElement)( "button", { type: "button", className: "bt-news-jump", onClick: onJump }, - (0, import_react9.createElement)("span", { className: "bt-news-ico" }, (0, import_react9.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), -+ (0, import_react17.createElement)("span", { className: "bt-news-ico" }, (0, import_react17.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), ++ (0, import_react18.createElement)("span", { className: "bt-news-ico" }, (0, import_react18.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), label(count) ), - (0, import_react9.createElement)("button", { -+ (0, import_react17.createElement)("button", { ++ (0, import_react18.createElement)("button", { type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), @@ -8408,7 +12343,7 @@ diff --git a/client.js b/client.js onDismiss(); } - }, (0, import_react9.createElement)(CloseIcon)) -+ }, (0, import_react17.createElement)(CloseIcon)) ++ }, (0, import_react18.createElement)(CloseIcon)) ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); @@ -8418,8 +12353,8 @@ diff --git a/client.js b/client.js return (0, import_react_dom4.createPortal)( - (0, import_react9.createElement)( - import_react9.Fragment, -+ (0, import_react17.createElement)( -+ import_react17.Fragment, ++ (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 @@ -8527,7 +12462,7 @@ diff --git a/client.js b/client.js + // src/client/composer.js -var import_react10 = require("react"); -+var import_react18 = require("react"); ++var import_react19 = require("react"); var import_react_dom5 = require("react-dom"); var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; var keepComposerFocus = (event) => event.preventDefault(); @@ -8536,15 +12471,15 @@ diff --git a/client.js b/client.js - const [note, setNote] = (0, import_react10.useState)(""); - const recognition = (0, import_react10.useRef)(null); - const latest = (0, import_react10.useRef)(onText); -+ const [listening, setListening] = (0, import_react18.useState)(false); -+ const [note, setNote] = (0, import_react18.useState)(""); -+ const recognition = (0, import_react18.useRef)(null); -+ const latest = (0, import_react18.useRef)(onText); ++ const [listening, setListening] = (0, import_react19.useState)(false); ++ const [note, setNote] = (0, import_react19.useState)(""); ++ const recognition = (0, import_react19.useRef)(null); ++ const latest = (0, import_react19.useRef)(onText); latest.current = onText; - (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); - (0, import_react10.useEffect)(() => { -+ (0, import_react18.useEffect)(() => () => recognition.current?.abort(), []); -+ (0, import_react18.useEffect)(() => { ++ (0, import_react19.useEffect)(() => () => recognition.current?.abort(), []); ++ (0, import_react19.useEffect)(() => { if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); @@ -8558,14 +12493,14 @@ diff --git a/client.js b/client.js + const self = roster.self; + const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0); + if (room2?.viewerRole === "invited-owner") { -+ return (0, import_react18.createElement)( ++ return (0, import_react19.createElement)( + "span", + { className: "bt-composer-as", title: t("You can remove your Bot from this room") }, + t("Shared room from {name}", { name: room2.sharedFrom?.inviterName || t("a friend") }) + ); + } + if (!self || !room2 || humans.length < 2) return null; -+ return (0, import_react18.createElement)( ++ return (0, import_react19.createElement)( + "span", + { className: "bt-composer-as", title: t("Messages in this room are attributed to you") }, + t("Sending as {name}", { name: self.displayName || self.memberId }) @@ -8577,7 +12512,7 @@ diff --git a/client.js b/client.js }); const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); - (0, import_react10.useEffect)(() => { -+ (0, import_react18.useEffect)(() => { ++ (0, import_react19.useEffect)(() => { if (!quote || !inputActions) return; actions.clearQuote(quote.token); const excerpt = quote.text.replace(/\s+/g, " ").trim(); @@ -8596,11 +12531,11 @@ diff --git a/client.js b/client.js const pluginMic = actions.speech() !== void 0; - return (0, import_react10.createElement)( - import_react10.Fragment, -+ return (0, import_react18.createElement)( -+ import_react18.Fragment, ++ return (0, import_react19.createElement)( ++ import_react19.Fragment, null, - pluginMic ? null : (0, import_react10.createElement)("button", { -+ pluginMic ? null : (0, import_react18.createElement)("button", { ++ pluginMic ? null : (0, import_react19.createElement)("button", { type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), @@ -8610,8 +12545,8 @@ diff --git a/client.js b/client.js onClick: () => dictation.listening ? dictation.stop() : dictation.start() - }, (0, import_react10.createElement)(MicIcon)), - (0, import_react10.createElement)("button", { -+ }, (0, import_react18.createElement)(MicIcon)), -+ (0, import_react18.createElement)("button", { ++ }, (0, import_react19.createElement)(MicIcon)), ++ (0, import_react19.createElement)("button", { type: "button", className: "bt-voice", "aria-label": t("Start voice chat"), @@ -8620,8 +12555,8 @@ diff --git a/client.js b/client.js onClick: () => actions.openVoice(sessionId) - }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), - dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null -+ }, (0, import_react18.createElement)(WaveIcon, { size: 18 })), -+ dictation.note ? (0, import_react18.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ++ }, (0, import_react19.createElement)(WaveIcon, { size: 18 })), ++ dictation.note ? (0, import_react19.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ); } function menuIcons(roster) { @@ -8630,21 +12565,21 @@ diff --git a/client.js b/client.js if (!row.bot) return PeopleIcon; const id = row.bot.id; - if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); -+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react18.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); ++ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react19.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); return marks.get(id); }; } function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { const roster = useRoster((value) => value); - const [open, setOpen] = (0, import_react10.useState)(null); -+ const [open, setOpen] = (0, import_react18.useState)(null); ++ const [open, setOpen] = (0, import_react19.useState)(null); const [shown, leaving] = useLinger(open, 120); - const button = (0, import_react10.useRef)(null); - const menu = (0, import_react10.useRef)(null); - (0, import_react10.useEffect)(() => { -+ const button = (0, import_react18.useRef)(null); -+ const menu = (0, import_react18.useRef)(null); -+ (0, import_react18.useEffect)(() => { ++ const button = (0, import_react19.useRef)(null); ++ const menu = (0, import_react19.useRef)(null); ++ (0, import_react19.useEffect)(() => { if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); @@ -8654,11 +12589,11 @@ diff --git a/client.js b/client.js const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); - return (0, import_react10.createElement)( - import_react10.Fragment, -+ return (0, import_react18.createElement)( -+ import_react18.Fragment, ++ return (0, import_react19.createElement)( ++ import_react19.Fragment, null, - (0, import_react10.createElement)("button", { -+ (0, import_react18.createElement)("button", { ++ (0, import_react19.createElement)("button", { ref: button, type: "button", className: "bt-plus", @@ -8668,8 +12603,8 @@ diff --git a/client.js b/client.js onClick: toggle - }, (0, import_react10.createElement)(PlusIcon)), - shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( -+ }, (0, import_react18.createElement)(PlusIcon)), -+ shown ? (0, import_react_dom5.createPortal)((0, import_react18.createElement)( ++ }, (0, import_react19.createElement)(PlusIcon)), ++ shown ? (0, import_react_dom5.createPortal)((0, import_react19.createElement)( "div", { ref: menu, @@ -8680,9 +12615,9 @@ diff --git a/client.js b/client.js - (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), - canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, - (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) -+ (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react18.createElement)(PaperclipIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -+ canReference ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react18.createElement)(ChatsIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -+ (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react18.createElement)(ClockIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) ++ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react19.createElement)(PaperclipIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), ++ canReference ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react19.createElement)(ChatsIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, ++ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react19.createElement)(ClockIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) ), document.body) : null ); } @@ -8868,10 +12803,31 @@ 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"]); -@@ -6121,232 +6121,8 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +@@ -6100,253 +6100,29 @@ async function openCapture() { + } }; } - +-async function transcribeWave(speech, catalog, wave, signal) { +- if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); +- const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); +- if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); +- return String(result.value?.text ?? "").trim(); +-} +-function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +- let spokeAt = 0; +- let quietSince = 0; +- return (level, now) => { +- if (level >= loud) { +- spokeAt ||= now; +- quietSince = 0; +- return "speech"; +- } +- if (!spokeAt) return "waiting"; +- if (level < quiet) quietSince ||= now; +- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; +- }; +-} +- -// src/client/overlay-hooks.js -var import_react11 = require("react"); -var escapeLayers = []; @@ -9095,10 +13051,30 @@ diff --git a/client.js b/client.js - ) - ); -} -- ++async function transcribeWave(speech, catalog, wave, signal) { ++ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); ++ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); ++ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); ++ return String(result.value?.text ?? "").trim(); ++} ++function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { ++ let spokeAt = 0; ++ let quietSince = 0; ++ return (level, now) => { ++ if (level >= loud) { ++ spokeAt ||= now; ++ quietSince = 0; ++ return "speech"; ++ } ++ if (!spokeAt) return "waiting"; ++ if (level < quiet) quietSince ||= now; ++ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; ++ }; ++} + // src/client/model-menu.js -var import_react13 = require("react"); -+var import_react19 = require("react"); ++var import_react20 = require("react"); var import_react_dom6 = require("react-dom"); var MENU_EXIT_MS = 120; function groupModels(models) { @@ -9117,8 +13093,8 @@ diff --git a/client.js b/client.js function useEfforts(served, ref, actions) { - const [fetched, setFetched] = (0, import_react13.useState)(void 0); - (0, import_react13.useEffect)(() => { -+ const [fetched, setFetched] = (0, import_react19.useState)(void 0); -+ (0, import_react19.useEffect)(() => { ++ const [fetched, setFetched] = (0, import_react20.useState)(void 0); ++ (0, import_react20.useEffect)(() => { setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; @@ -9128,15 +13104,15 @@ diff --git a/client.js b/client.js function EffortRows({ reasoning, current, onPick }) { - return (0, import_react13.createElement)( - import_react13.Fragment, -+ return (0, import_react19.createElement)( -+ import_react19.Fragment, ++ return (0, import_react20.createElement)( ++ import_react20.Fragment, null, - (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), - effortRows(reasoning).map((row) => (0, import_react13.createElement)( -+ (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -+ effortRows(reasoning).map((row) => (0, import_react19.createElement)( ++ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), ++ effortRows(reasoning).map((row) => (0, import_react20.createElement)( "button", { key: row.effort ?? "default", @@ -9146,21 +13122,21 @@ diff --git a/client.js b/client.js }, - (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), - (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) -+ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, row.label), -+ (0, import_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react19.createElement)(CheckIcon) : null) ++ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, row.label), ++ (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react20.createElement)(CheckIcon) : null) )) ); } function ModelRows({ roster, current, onPick }) { - return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)( - import_react13.Fragment, -+ return groupModels(roster.models).map(([provider, list]) => (0, import_react19.createElement)( -+ import_react19.Fragment, ++ return groupModels(roster.models).map(([provider, list]) => (0, import_react20.createElement)( ++ import_react20.Fragment, { key: list[0].provider ?? provider }, - (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), - list.map((model) => (0, import_react13.createElement)( -+ (0, import_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -+ list.map((model) => (0, import_react19.createElement)( ++ (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), ++ list.map((model) => (0, import_react20.createElement)( "button", { key: model.ref, @@ -9171,25 +13147,25 @@ diff --git a/client.js b/client.js - (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), - model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, - (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) -+ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, model.name), -+ model.ref === roster.defaultModel ? (0, import_react19.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -+ (0, import_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react19.createElement)(CheckIcon) : null) ++ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, model.name), ++ model.ref === roster.defaultModel ? (0, import_react20.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, ++ (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react20.createElement)(CheckIcon) : null) )) )); } -var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); -+var Caret = () => (0, import_react19.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react19.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); ++var Caret = () => (0, import_react20.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react20.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); function useAnchoredMenu() { - const [place, setPlace] = (0, import_react13.useState)(null); -+ const [place, setPlace] = (0, import_react19.useState)(null); ++ const [place, setPlace] = (0, import_react20.useState)(null); const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); - const button = (0, import_react13.useRef)(null); - const menu = (0, import_react13.useRef)(null); -+ const button = (0, import_react19.useRef)(null); -+ const menu = (0, import_react19.useRef)(null); ++ const button = (0, import_react20.useRef)(null); ++ const menu = (0, import_react20.useRef)(null); const close = () => setPlace(null); - (0, import_react13.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { ++ (0, import_react20.useEffect)(() => { if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; @@ -9207,11 +13183,11 @@ diff --git a/client.js b/client.js const models = roster.models ?? []; - return (0, import_react13.createElement)( - import_react13.Fragment, -+ return (0, import_react19.createElement)( -+ import_react19.Fragment, ++ return (0, import_react20.createElement)( ++ import_react20.Fragment, null, - (0, import_react13.createElement)( -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( "button", { ref: button, @@ -9220,34 +13196,34 @@ diff --git a/client.js b/client.js onClick: toggle }, - (0, import_react13.createElement)( -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( "span", { className: "bt-model-copy" }, - (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), - shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null -+ (0, import_react19.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -+ shown ? (0, import_react19.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null ++ (0, import_react20.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), ++ shown ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null ), - (0, import_react13.createElement)(Caret) -+ (0, import_react19.createElement)(Caret) ++ (0, import_react20.createElement)(Caret) ), - shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( -+ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react19.createElement)( ++ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react20.createElement)( "div", { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, - models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), - effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, - (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -+ models.length > 0 ? (0, import_react19.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react19.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -+ effortRows(reasoning).length > 0 ? (0, import_react19.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -+ (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => { ++ models.length > 0 ? (0, import_react20.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), ++ effortRows(reasoning).length > 0 ? (0, import_react20.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, ++ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => { close(); - openHarnessSettings(); - } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) + openHarnessModels(); -+ } }, (0, import_react19.createElement)(SlidersIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) ++ } }, (0, import_react20.createElement)(SlidersIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) ), document.body) : null ); } @@ -9256,15 +13232,15 @@ diff --git a/client.js b/client.js const bot = roster.byId[sessionId]; - const [busy, setBusy] = (0, import_react13.useState)(""); - const [error, setError] = (0, import_react13.useState)(""); -+ const [busy, setBusy] = (0, import_react19.useState)(""); -+ const [error, setError] = (0, import_react19.useState)(""); ++ const [busy, setBusy] = (0, import_react20.useState)(""); ++ const [error, setError] = (0, import_react20.useState)(""); const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; - (0, import_react13.useEffect)(() => { -+ (0, import_react19.useEffect)(() => { ++ (0, import_react20.useEffect)(() => { if (shown) void actions.refreshModels?.(); }, [shown, sessionId]); if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; -+ if (bot.remote) return bot.remote.gone ? (0, import_react19.createElement)(RemoteGoneDock, { bot }) : null; ++ if (bot.remote) return bot.remote.gone ? (0, import_react20.createElement)(RemoteGoneDock, { bot }) : null; const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; @@ -9274,14 +13250,14 @@ diff --git a/client.js b/client.js const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); - const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ - ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( -+ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react19.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react19.createElement)(KeyHelpLink, { key: "key-help" })] : [ -+ ...groupModels(models).map(([provider, list]) => (0, import_react19.createElement)( ++ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" })] : [ ++ ...groupModels(models).map(([provider, list]) => (0, import_react20.createElement)( "div", { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, - (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), - (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { -+ (0, import_react19.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -+ (0, import_react19.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react19.createElement)("button", { ++ (0, import_react20.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), ++ (0, import_react20.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react20.createElement)("button", { key: model.ref, type: "button", className: "bt-model-chip", @@ -9290,55 +13266,55 @@ diff --git a/client.js b/client.js title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) - }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -+ }, model.name, busy === model.ref ? (0, import_react19.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) ++ }, model.name, busy === model.ref ? (0, import_react20.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) )), - (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) -+ (0, import_react19.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react19.createElement)(SlidersIcon), t("Manage models")) ++ (0, import_react20.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react20.createElement)(SlidersIcon), t("Manage models")) ]; - return (0, import_react13.createElement)( -+ return (0, import_react19.createElement)( ++ return (0, import_react20.createElement)( "div", { className: "bt-model-dock" }, - (0, import_react13.createElement)( -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( "div", { className: "bt-astack", "data-lead": "" }, - (0, import_react13.createElement)( -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( "div", { className: "bt-msg" }, - (0, import_react13.createElement)( -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( "div", { className: "bt-msg-line" }, - (0, import_react13.createElement)(BotMark, { bot, size: 22 }), - (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) -+ (0, import_react19.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react19.createElement)("div", { className: "bt-bubble" }, text) ++ (0, import_react20.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react20.createElement)("div", { className: "bt-bubble" }, text) ) ) ), - (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), - error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -+ (0, import_react19.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -+ error ? (0, import_react19.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null ++ (0, import_react20.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), ++ error ? (0, import_react20.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null + ); +} +function RemoteGoneDock({ bot }) { -+ return (0, import_react19.createElement)( ++ return (0, import_react20.createElement)( + "div", + { className: "bt-model-dock", "data-flybot": "remote-gone" }, -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( + "div", + { className: "bt-astack", "data-lead": "" }, -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( + "div", + { className: "bt-msg" }, -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)( + "div", + { className: "bt-msg-line" }, -+ (0, import_react19.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react19.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) ++ (0, import_react20.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react20.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) + ) + ) + ) @@ -9354,7 +13330,7 @@ diff --git a/client.js b/client.js } +var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; +function KeyHelpLink() { -+ return (0, import_react19.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); ++ return (0, import_react20.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); +} +function missingModelKey(message) { + const text = String(message ?? ""); @@ -9378,22 +13354,22 @@ diff --git a/client.js b/client.js + const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw; + const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []; + const deepseek = missing ? /^deepseek/i.test(missing.route) : false; -+ return (0, import_react19.createElement)( ++ return (0, import_react20.createElement)( + "div", + { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 }, -+ (0, import_react19.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), -+ (0, import_react19.createElement)( ++ (0, import_react20.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }), ++ (0, import_react20.createElement)( + "div", + { className: "bt-turn-error-copy" }, -+ (0, import_react19.createElement)("span", { className: "bt-turn-error-title" }, title), -+ (0, import_react19.createElement)("span", { className: "bt-turn-error-message" }, message), -+ code ? (0, import_react19.createElement)("code", { className: "bt-turn-error-code" }, code) : null ++ (0, import_react20.createElement)("span", { className: "bt-turn-error-title" }, title), ++ (0, import_react20.createElement)("span", { className: "bt-turn-error-message" }, message), ++ code ? (0, import_react20.createElement)("code", { className: "bt-turn-error-code" }, code) : null + ), -+ choices.length > 0 ? (0, import_react19.createElement)( ++ choices.length > 0 ? (0, import_react20.createElement)( + "div", + { className: "bt-turn-error-actions" }, -+ choices.map(([label, run]) => (0, import_react19.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), -+ deepseek ? (0, import_react19.createElement)(KeyHelpLink, { key: "key-help" }) : null ++ choices.map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), ++ deepseek ? (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" }) : null + ) : null + ); +} @@ -9402,18 +13378,18 @@ diff --git a/client.js b/client.js const entry = open ? actions.signInEntry?.() : null; if (!entry) return null; - return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); -+ return (0, import_react19.createElement)(SignInHost, { key: open, entry, actions }); ++ return (0, import_react20.createElement)(SignInHost, { key: open, entry, actions }); } function SignInHost({ entry, actions }) { - const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); -+ const [ops] = (0, import_react19.useState)(() => entry.inject?.() ?? {}); ++ const [ops] = (0, import_react20.useState)(() => entry.inject?.() ?? {}); const close = () => actions.closeSignIn(); - const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); - const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); - return (0, import_react13.createElement)(entry.component, { -+ const useHook = (source) => (selector) => (0, import_react19.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -+ const [hooks] = (0, import_react19.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -+ return (0, import_react19.createElement)(entry.component, { ++ const useHook = (source) => (selector) => (0, import_react20.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); ++ const [hooks] = (0, import_react20.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); ++ return (0, import_react20.createElement)(entry.component, { ...ops, ...hooks, t: actions.accountT(), @@ -9439,7 +13415,7 @@ diff --git a/client.js b/client.js `; +// src/client/model-test.js -+var import_react20 = require("react"); ++var import_react21 = require("react"); +var FAILURE_COPY = { + NO_MODEL: "Pick a model first.", + NO_KEY: "This model has no API key yet. Set one up in the model settings.", @@ -9464,12 +13440,12 @@ diff --git a/client.js b/client.js +} +var testModel = (id) => call("test-model", { id }); +function ModelTestRow({ bot, modelName, test = testModel }) { -+ const [state, setState] = (0, import_react20.useState)({ busy: false, outcome: void 0 }); -+ const live = (0, import_react20.useRef)(true); -+ (0, import_react20.useEffect)(() => () => { ++ const [state, setState] = (0, import_react21.useState)({ busy: false, outcome: void 0 }); ++ const live = (0, import_react21.useRef)(true); ++ (0, import_react21.useEffect)(() => () => { + live.current = false; + }, []); -+ (0, import_react20.useEffect)(() => { ++ (0, import_react21.useEffect)(() => { + setState({ busy: false, outcome: void 0 }); + }, [bot.id, bot.model]); + const start = () => { @@ -9483,21 +13459,21 @@ diff --git a/client.js b/client.js + }); + }; + const { busy, outcome } = state; -+ return (0, import_react20.createElement)( ++ return (0, import_react21.createElement)( + "div", + { className: "bt-set-row bt-bs-row bt-model-test" }, -+ (0, import_react20.createElement)( ++ (0, import_react21.createElement)( + "span", + { className: "bt-set-copy" }, -+ (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), -+ (0, import_react20.createElement)( ++ (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), ++ (0, import_react21.createElement)( + "span", + { className: "bt-model-test-result", role: "status", "aria-live": "polite", "data-ok": outcome === void 0 ? void 0 : String(outcome.ok) }, + busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, -+ outcome?.code ? (0, import_react20.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null ++ outcome?.code ? (0, import_react21.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null + ) + ), -+ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ++ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) + ); +} +var MODEL_TEST_CSS = ` @@ -9510,11 +13486,11 @@ diff --git a/client.js b/client.js + // src/client/folder-menu.js -var import_react14 = require("react"); -+var import_react21 = require("react"); ++var import_react22 = require("react"); var import_react_dom7 = require("react-dom"); var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); -var Tick = ({ on }) => (0, import_react14.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react14.createElement)(CheckIcon) : null); -+var Tick = ({ on }) => (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react21.createElement)(CheckIcon) : null); ++var Tick = ({ on }) => (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react22.createElement)(CheckIcon) : null); function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); - const [info, setInfo] = (0, import_react14.useState)(null); @@ -9522,11 +13498,11 @@ diff --git a/client.js b/client.js - const [draft, setDraft] = (0, import_react14.useState)(""); - const [listing, setListing] = (0, import_react14.useState)(null); - const [problem, setProblem] = (0, import_react14.useState)(""); -+ const [info, setInfo] = (0, import_react21.useState)(null); -+ const [view, setView] = (0, import_react21.useState)("list"); -+ const [draft, setDraft] = (0, import_react21.useState)(""); -+ const [listing, setListing] = (0, import_react21.useState)(null); -+ const [problem, setProblem] = (0, import_react21.useState)(""); ++ const [info, setInfo] = (0, import_react22.useState)(null); ++ const [view, setView] = (0, import_react22.useState)("list"); ++ const [draft, setDraft] = (0, import_react22.useState)(""); ++ const [listing, setListing] = (0, import_react22.useState)(null); ++ const [problem, setProblem] = (0, import_react22.useState)(""); const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { @@ -9535,50 +13511,50 @@ diff --git a/client.js b/client.js const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; - const listView = (0, import_react14.createElement)( -+ const listView = (0, import_react21.createElement)( ++ const listView = (0, import_react22.createElement)( "div", { key: "list", className: "bt-folder-view" }, - (0, import_react14.createElement)( -+ (0, import_react21.createElement)( ++ (0, import_react22.createElement)( "button", { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, - (0, import_react14.createElement)("span", { className: "bt-menu-label" }, fallback), - (0, import_react14.createElement)(Tick, { on: !value }) -+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, fallback), -+ (0, import_react21.createElement)(Tick, { on: !value }) ++ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, fallback), ++ (0, import_react22.createElement)(Tick, { on: !value }) ), - folders.length > 0 ? (0, import_react14.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, - folders.map((folder) => (0, import_react14.createElement)( -+ folders.length > 0 ? (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -+ folders.map((folder) => (0, import_react21.createElement)( ++ folders.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, ++ folders.map((folder) => (0, import_react22.createElement)( "button", { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, - (0, import_react14.createElement)("span", { className: "bt-folder-copy" }, (0, import_react14.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react14.createElement)("span", { className: "bt-folder-path" }, folder.path)), - (0, import_react14.createElement)(Tick, { on: folder.path === value }) -+ (0, import_react21.createElement)("span", { className: "bt-folder-copy" }, (0, import_react21.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react21.createElement)("span", { className: "bt-folder-path" }, folder.path)), -+ (0, import_react21.createElement)(Tick, { on: folder.path === value }) ++ (0, import_react22.createElement)("span", { className: "bt-folder-copy" }, (0, import_react22.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react22.createElement)("span", { className: "bt-folder-path" }, folder.path)), ++ (0, import_react22.createElement)(Tick, { on: folder.path === value }) )), - (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - info?.picker === "native" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, - info?.picker === "browse" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, - (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ info?.picker === "native" ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -+ info?.picker === "browse" ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -+ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => { ++ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ info?.picker === "native" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, ++ info?.picker === "browse" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, ++ (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => { setDraft(value ?? ""); setView("type"); - } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) -+ } }, (0, import_react21.createElement)(PencilIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) ++ } }, (0, import_react22.createElement)(PencilIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) ); - const typeView = (0, import_react14.createElement)( -+ const typeView = (0, import_react21.createElement)( ++ const typeView = (0, import_react22.createElement)( "div", { key: "type", className: "bt-folder-view" }, - (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), - (0, import_react14.createElement)("input", { -+ (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react21.createElement)(ChevronLeftIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react21.createElement)("input", { ++ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))), ++ (0, import_react22.createElement)("input", { className: "bt-folder-input", autoFocus: true, value: draft, @@ -9587,48 +13563,48 @@ diff --git a/client.js b/client.js } }), - (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) -+ (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) ++ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) ); const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; - const browseView = listing ? (0, import_react14.createElement)( -+ const browseView = listing ? (0, import_react21.createElement)( ++ const browseView = listing ? (0, import_react22.createElement)( "div", { key: `browse:${listing.path}`, className: "bt-folder-view" }, - (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), - (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), - parent ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, - inside.map((entry) => (0, import_react14.createElement)( -+ (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react21.createElement)(ChevronLeftIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -+ (0, import_react21.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -+ parent ? (0, import_react21.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react21.createElement)(FolderIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -+ inside.map((entry) => (0, import_react21.createElement)( ++ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))), ++ (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), ++ parent ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, ++ inside.map((entry) => (0, import_react22.createElement)( "button", { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, - (0, import_react14.createElement)(FolderIcon), - (0, import_react14.createElement)("span", { className: "bt-menu-label" }, entry.name), - (0, import_react14.createElement)(ChevronRightIcon) -+ (0, import_react21.createElement)(FolderIcon), -+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, entry.name), -+ (0, import_react21.createElement)(ChevronRightIcon) ++ (0, import_react22.createElement)(FolderIcon), ++ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, entry.name), ++ (0, import_react22.createElement)(ChevronRightIcon) )), - inside.length === 0 ? (0, import_react14.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, - listing.truncated ? (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, - (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), - (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) -+ inside.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -+ listing.truncated ? (0, import_react21.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -+ (0, import_react21.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) ++ inside.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, ++ listing.truncated ? (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, ++ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }), ++ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) ) : null; const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; - return (0, import_react14.createElement)( - import_react14.Fragment, -+ return (0, import_react21.createElement)( -+ import_react21.Fragment, ++ return (0, import_react22.createElement)( ++ import_react22.Fragment, null, - (0, import_react14.createElement)( -+ (0, import_react21.createElement)( ++ (0, import_react22.createElement)( "button", { ref: button, @@ -9637,24 +13613,24 @@ diff --git a/client.js b/client.js onClick: toggle }, - (0, import_react14.createElement)( -+ (0, import_react21.createElement)( ++ (0, import_react22.createElement)( "span", { className: "bt-model-copy" }, - (0, import_react14.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), - value ? (0, import_react14.createElement)("span", { className: "bt-model-provider" }, value) : null -+ (0, import_react21.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -+ value ? (0, import_react21.createElement)("span", { className: "bt-model-provider" }, value) : null ++ (0, import_react22.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), ++ value ? (0, import_react22.createElement)("span", { className: "bt-model-provider" }, value) : null ), - (0, import_react14.createElement)(Caret) -+ (0, import_react21.createElement)(Caret) ++ (0, import_react22.createElement)(Caret) ), - shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( -+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react21.createElement)( ++ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react22.createElement)( "div", { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, shownView, - problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null -+ problem ? (0, import_react21.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null ++ problem ? (0, import_react22.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null ), document.body) : null ); } @@ -9664,7 +13640,7 @@ diff --git a/client.js b/client.js // src/client/schedules.js -var import_react15 = require("react"); -var POLL_MS = 3e4; -+var import_react22 = require("react"); ++var import_react23 = require("react"); +var POLL_MS5 = 3e4; var shared = null; var listeners = /* @__PURE__ */ new Set(); @@ -9675,8 +13651,8 @@ diff --git a/client.js b/client.js function useSchedules(actions) { - const [view, setView] = (0, import_react15.useState)(shared); - (0, import_react15.useEffect)(() => { -+ const [view, setView] = (0, import_react22.useState)(shared); -+ (0, import_react22.useEffect)(() => { ++ const [view, setView] = (0, import_react23.useState)(shared); ++ (0, import_react23.useEffect)(() => { listeners.add(setView); let stopped = false; let timer; @@ -9714,9 +13690,9 @@ diff --git a/client.js b/client.js + } +} +function TaskRow({ task, actions, onView, onEdit, showBot, roster }) { -+ const [confirming, setConfirming] = (0, import_react22.useState)(false); -+ const [busy, setBusy] = (0, import_react22.useState)(false); -+ (0, import_react22.useEffect)(() => { ++ const [confirming, setConfirming] = (0, import_react23.useState)(false); ++ const [busy, setBusy] = (0, import_react23.useState)(false); ++ (0, import_react23.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); @@ -9732,7 +13708,7 @@ diff --git a/client.js b/client.js + onView(next); + }, () => setBusy(false)); + }; -+ const [retryError, setRetryError] = (0, import_react22.useState)(""); ++ const [retryError, setRetryError] = (0, import_react23.useState)(""); + const retry = () => { + setBusy(true); + setRetryError(""); @@ -9749,32 +13725,32 @@ diff --git a/client.js b/client.js + const stoppable = task.status === "active" || task.status === "paused"; + const failed = task.lastRun?.result === "failed"; + const last = lastRunText(task.lastRun); -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( "div", - { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, - showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, - (0, import_react15.createElement)( + { className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 }, -+ showBot ? (0, import_react22.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -+ (0, import_react22.createElement)( ++ showBot ? (0, import_react23.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, ++ (0, import_react23.createElement)( "span", { className: "bt-set-copy" }, - (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), - (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), - task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null -+ (0, import_react22.createElement)("span", { className: "bt-task-title" }, task.title), -+ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), -+ task.prompt && task.prompt !== task.title ? (0, import_react22.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, -+ last ? (0, import_react22.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, -+ retryError ? (0, import_react22.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null ++ (0, import_react23.createElement)("span", { className: "bt-task-title" }, task.title), ++ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), ++ task.prompt && task.prompt !== task.title ? (0, import_react23.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null, ++ last ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null, ++ retryError ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null ), - (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), - (0, import_react15.createElement)("button", { -+ (0, import_react22.createElement)("span", { className: "bt-task-next" }, nextText(task)), -+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, -+ onEdit ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react22.createElement)(PencilIcon)) : null, -+ stoppable ? (0, import_react22.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, -+ (0, import_react22.createElement)("button", { ++ (0, import_react23.createElement)("span", { className: "bt-task-next" }, nextText(task)), ++ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null, ++ onEdit ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react23.createElement)(PencilIcon)) : null, ++ stoppable ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null, ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, @@ -9783,7 +13759,7 @@ diff --git a/client.js b/client.js title: confirming ? void 0 : t("Delete task"), onClick: remove - }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) -+ }, confirming ? t("Delete") : (0, import_react22.createElement)(TrashIcon)) ++ }, confirming ? t("Delete") : (0, import_react23.createElement)(TrashIcon)) ); } var pad = (number) => String(number).padStart(2, "0"); @@ -9825,17 +13801,17 @@ diff --git a/client.js b/client.js +} +function TaskForm({ roster, actions, start, task, onDone, onCancel }) { + const first = formStart(task); -+ const [target, setTarget] = (0, import_react22.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? ""); -+ const [title, setTitle] = (0, import_react22.useState)(first.title); -+ const [prompt, setPrompt] = (0, import_react22.useState)(first.prompt); -+ const [kind, setKind] = (0, import_react22.useState)(first.kind); -+ const [at, setAt] = (0, import_react22.useState)(first.at); -+ const [time, setTime] = (0, import_react22.useState)(first.time); -+ const [weekdays, setWeekdays] = (0, import_react22.useState)(first.weekdays); -+ const [count, setCount] = (0, import_react22.useState)(first.count); -+ const [unit, setUnit] = (0, import_react22.useState)(first.unit); -+ const [busy, setBusy] = (0, import_react22.useState)(false); -+ const [error, setError] = (0, import_react22.useState)(""); ++ const [target, setTarget] = (0, import_react23.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? ""); ++ const [title, setTitle] = (0, import_react23.useState)(first.title); ++ const [prompt, setPrompt] = (0, import_react23.useState)(first.prompt); ++ const [kind, setKind] = (0, import_react23.useState)(first.kind); ++ const [at, setAt] = (0, import_react23.useState)(first.at); ++ const [time, setTime] = (0, import_react23.useState)(first.time); ++ const [weekdays, setWeekdays] = (0, import_react23.useState)(first.weekdays); ++ const [count, setCount] = (0, import_react23.useState)(first.count); ++ const [unit, setUnit] = (0, import_react23.useState)(first.unit); ++ const [busy, setBusy] = (0, import_react23.useState)(false); ++ const [error, setError] = (0, import_react23.useState)(""); const timing = () => { - if (kind === "at") return { kind, at: new Date(at).toISOString() }; + if (kind === "cron") return { kind: "keep" }; @@ -9859,60 +13835,60 @@ diff --git a/client.js b/client.js }; const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); - return (0, import_react15.createElement)( -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( "form", { className: "bt-set-card bt-task-form", onSubmit: submit }, - (0, import_react15.createElement)( -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row" }, - (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), ++ (0, import_react23.createElement)( "select", { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, - (0, import_react15.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react15.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), - roster.rooms.length > 0 ? (0, import_react15.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react15.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null -+ (0, import_react22.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react22.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -+ roster.rooms.length > 0 ? (0, import_react22.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react22.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null ++ (0, import_react23.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react23.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), ++ roster.rooms.length > 0 ? (0, import_react23.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react23.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null ) ), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row" }, - (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Name"))), - (0, import_react15.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react22.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), ++ (0, import_react23.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) ), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( "label", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), - (0, import_react15.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -+ (0, import_react22.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), ++ (0, import_react23.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) ), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("When"))), - (0, import_react15.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react15.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("When"))), -+ (0, import_react22.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react22.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("When"))), ++ (0, import_react23.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react23.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), ++ (0, import_react23.createElement)( "div", { key: kind, className: "bt-task-when bt-veil" }, - kind === "at" ? (0, import_react15.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, - kind === "weekly" ? (0, import_react15.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react15.createElement)("button", { -+ kind === "cron" ? (0, import_react22.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, -+ kind === "at" ? (0, import_react22.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, -+ kind === "weekly" ? (0, import_react22.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react22.createElement)("button", { ++ kind === "cron" ? (0, import_react23.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null, ++ kind === "at" ? (0, import_react23.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, ++ kind === "weekly" ? (0, import_react23.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react23.createElement)("button", { key: day, type: "button", className: "bt-task-day", @@ -9922,34 +13898,34 @@ diff --git a/client.js b/client.js - kind === "daily" || kind === "weekly" ? (0, import_react15.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, - kind === "every" ? (0, import_react15.createElement)( - import_react15.Fragment, -+ kind === "daily" || kind === "weekly" ? (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, -+ kind === "every" ? (0, import_react22.createElement)( -+ import_react22.Fragment, ++ kind === "daily" || kind === "weekly" ? (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, ++ kind === "every" ? (0, import_react23.createElement)( ++ import_react23.Fragment, null, - (0, import_react15.createElement)("span", { className: "bt-task-every" }, t("Every")), - (0, import_react15.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)("span", { className: "bt-task-every" }, t("Every")), -+ (0, import_react22.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("span", { className: "bt-task-every" }, t("Every")), ++ (0, import_react23.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), ++ (0, import_react23.createElement)( "select", { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, - UNITS.map(([id, , label]) => (0, import_react15.createElement)("option", { key: id, value: id }, t(label))) -+ UNITS.map(([id, , label]) => (0, import_react22.createElement)("option", { key: id, value: id }, t(label))) ++ UNITS.map(([id, , label]) => (0, import_react23.createElement)("option", { key: id, value: id }, t(label))) ) ) : null ) ), - error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, - (0, import_react15.createElement)( -+ error ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ (0, import_react22.createElement)( ++ error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, ++ (0, import_react23.createElement)( "div", { className: "bt-set-row bt-task-foot" }, - (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), - (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) -+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), ++ (0, import_react23.createElement)( + "button", + { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, + task ? busy ? t("Saving…") : t("Save changes") : busy ? t("Creating…") : t("Create task") @@ -9964,25 +13940,25 @@ diff --git a/client.js b/client.js + actions.selectPanel("plugins"); +} +function AutomationOff({ actions }) { -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( "div", { className: "bt-set-card bt-task-off" }, - (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), - (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), - (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), - (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -+ (0, import_react22.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react22.createElement)(ClockIcon)), -+ (0, import_react22.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -+ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), -+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ++ (0, import_react23.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react23.createElement)(ClockIcon)), ++ (0, import_react23.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), ++ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")), ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) + ); +} +function QuietHoursCard({ view, actions }) { + const quiet = view.quietHours ?? { on: false }; -+ const [busy, setBusy] = (0, import_react22.useState)(false); -+ const [error, setError] = (0, import_react22.useState)(""); -+ const [from, setFrom] = (0, import_react22.useState)(quiet.from ?? "22:00"); -+ const [to, setTo] = (0, import_react22.useState)(quiet.to ?? "07:00"); ++ const [busy, setBusy] = (0, import_react23.useState)(false); ++ const [error, setError] = (0, import_react23.useState)(""); ++ const [from, setFrom] = (0, import_react23.useState)(quiet.from ?? "22:00"); ++ const [to, setTo] = (0, import_react23.useState)(quiet.to ?? "07:00"); + const unchecked = view.quietUnchecked === true; + const refresh = () => { + void Promise.resolve(actions.schedules?.()).then((next) => { @@ -10000,23 +13976,23 @@ diff --git a/client.js b/client.js + refresh(); + }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false)); + }; -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( + "section", + { className: "bt-set-section" }, -+ (0, import_react22.createElement)("h3", null, t("Quiet hours")), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("h3", null, t("Quiet hours")), ++ (0, import_react23.createElement)( + "div", + { className: "bt-set-card" }, -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( + "span", + { className: "bt-set-copy" }, -+ (0, import_react22.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), -+ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) ++ (0, import_react23.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")), ++ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists.")) + ), -+ (0, import_react22.createElement)("button", { ++ (0, import_react23.createElement)("button", { + type: "button", + className: "bt-soft bt-soft-sm", + disabled: busy, @@ -10024,21 +14000,21 @@ diff --git a/client.js b/client.js + onClick: () => save(quiet.on ? { on: false } : { on: true, from, to }) + }, quiet.on ? t("Turn off") : t("Turn on")) + ), -+ quiet.on ? (0, import_react22.createElement)( ++ quiet.on ? (0, import_react23.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Window"))), -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Window"))), ++ (0, import_react23.createElement)( + "div", + { className: "bt-task-when" }, -+ (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), -+ (0, import_react22.createElement)("span", { className: "bt-task-every" }, "–"), -+ (0, import_react22.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), -+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) ++ (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }), ++ (0, import_react23.createElement)("span", { className: "bt-task-every" }, "–"), ++ (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }), ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window")) + ) + ) : null, -+ error ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -+ quiet.on && unchecked ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null ++ error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, ++ quiet.on && unchecked ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null + ) ); } @@ -10047,10 +14023,10 @@ diff --git a/client.js b/client.js - const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); - if (view === null) return (0, import_react15.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); - if (!view.available) return (0, import_react15.createElement)(AutomationOff); -+ const [adding, setAdding] = (0, import_react22.useState)(Boolean(start)); -+ const [editing, setEditing] = (0, import_react22.useState)(null); -+ if (view === null) return (0, import_react22.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -+ if (!view.available) return (0, import_react22.createElement)(AutomationOff, { actions }); ++ const [adding, setAdding] = (0, import_react23.useState)(Boolean(start)); ++ const [editing, setEditing] = (0, import_react23.useState)(null); ++ if (view === null) return (0, import_react23.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); ++ if (!view.available) return (0, import_react23.createElement)(AutomationOff, { actions }); const shown = (next) => { publish(next); setAdding(false); @@ -10063,54 +14039,54 @@ diff --git a/client.js b/client.js const byChat = [ - ...roster.bots.map((bot) => [bot.id, (0, import_react15.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), - ...roster.rooms.map((room2) => [room2.id, (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) -+ ...roster.bots.map((bot) => [bot.id, (0, import_react22.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), -+ ...roster.rooms.map((room2) => [room2.id, (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) ++ ...roster.bots.map((bot) => [bot.id, (0, import_react23.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), ++ ...roster.rooms.map((room2) => [room2.id, (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) ].filter(([, , , tasks]) => tasks.length > 0); return [ - adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))), - byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)( -+ (0, import_react22.createElement)(QuietHoursCard, { key: "quiet", view, actions }), -+ adding ? (0, import_react22.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react22.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { ++ (0, import_react23.createElement)(QuietHoursCard, { key: "quiet", view, actions }), ++ adding ? (0, import_react23.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react23.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => { + setEditing(null); + setAdding(true); -+ } }, (0, import_react22.createElement)(PlusIcon), t("New task"))), -+ byChat.length === 0 && !adding ? (0, import_react22.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react22.createElement)( ++ } }, (0, import_react23.createElement)(PlusIcon), t("New task"))), ++ byChat.length === 0 && !adding ? (0, import_react23.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react23.createElement)( "section", { key: id, className: "bt-set-section" }, - (0, import_react15.createElement)("h3", { className: "bt-task-head" }, avatar, name), - (0, import_react15.createElement)("div", { className: "bt-set-card" }, tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish }))) -+ (0, import_react22.createElement)("h3", { className: "bt-task-head" }, avatar, name), -+ (0, import_react22.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) ++ (0, import_react23.createElement)("h3", { className: "bt-task-head" }, avatar, name), ++ (0, import_react23.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) }) )) ]; } +function TaskList({ tasks, editing, roster, actions, onEdit, onDone, onCancel, extra = [] }) { + const open = editing ? tasks.find((task) => task.id === editing.id && task.sessionId === editing.sessionId) : void 0; -+ const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react22.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit })); -+ return (0, import_react22.createElement)( -+ import_react22.Fragment, ++ const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react23.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit })); ++ return (0, import_react23.createElement)( ++ import_react23.Fragment, + null, -+ open ? (0, import_react22.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, -+ rows.length + extra.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null ++ open ? (0, import_react23.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null, ++ rows.length + extra.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null + ); +} function BotSchedules({ bot, roster, actions }) { const view = useSchedules(actions); -+ const [editing, setEditing] = (0, import_react22.useState)(null); ++ const [editing, setEditing] = (0, import_react23.useState)(null); if (view === null) return null; const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; - return (0, import_react15.createElement)( -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( "section", { className: "bt-set-section" }, - (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), - (0, import_react15.createElement)( -+ (0, import_react22.createElement)("h3", null, t("Scheduled tasks")), -+ !view.available ? (0, import_react22.createElement)( ++ (0, import_react23.createElement)("h3", null, t("Scheduled tasks")), ++ !view.available ? (0, import_react23.createElement)( "div", { className: "bt-set-card" }, - !view.available ? (0, import_react15.createElement)( -+ (0, import_react22.createElement)( ++ (0, import_react23.createElement)( "div", { className: "bt-set-row bt-bs-row" }, - (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings."))), @@ -10120,10 +14096,10 @@ diff --git a/client.js b/client.js - (0, import_react15.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react15.createElement)(PlusIcon), t("New task")) - ] - ) -+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), -+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) ++ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))), ++ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins")) + ) -+ ) : (0, import_react22.createElement)(TaskList, { ++ ) : (0, import_react23.createElement)(TaskList, { + tasks, + editing, + roster, @@ -10134,7 +14110,7 @@ diff --git a/client.js b/client.js + setEditing(null); + }, + onCancel: () => setEditing(null), -+ extra: [(0, import_react22.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react22.createElement)(PlusIcon), t("New task"))] ++ extra: [(0, import_react23.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react23.createElement)(PlusIcon), t("New task"))] + }) ); } @@ -10142,21 +14118,21 @@ diff --git a/client.js b/client.js const view = useSchedules(actions); const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; - return (0, import_react15.createElement)( -+ return (0, import_react22.createElement)( ++ return (0, import_react23.createElement)( "div", null, - (0, import_react15.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), - tasks.length === 0 ? (0, import_react15.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page in Harness settings.") : t("No scheduled tasks yet. Add one")) : (0, import_react15.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react15.createElement)( -+ (0, import_react22.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), -+ tasks.length === 0 ? (0, import_react22.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react22.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react22.createElement)( ++ (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")), ++ tasks.length === 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react23.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react23.createElement)( "button", { key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") }, - (0, import_react15.createElement)(ClockIcon), - (0, import_react15.createElement)("span", { className: "bt-task-line-title" }, task.title), - (0, import_react15.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) -+ (0, import_react22.createElement)(ClockIcon), -+ (0, import_react22.createElement)("span", { className: "bt-task-line-title" }, task.title), -+ (0, import_react22.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) ++ (0, import_react23.createElement)(ClockIcon), ++ (0, import_react23.createElement)("span", { className: "bt-task-line-title" }, task.title), ++ (0, import_react23.createElement)("span", { className: "bt-task-line-when" }, timingText(task)) ))) ); } @@ -10178,7 +14154,7 @@ diff --git a/client.js b/client.js // src/client/capability-settings.js -var import_react16 = require("react"); -+var import_react23 = require("react"); ++var import_react24 = require("react"); var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); var parseMap = (text, label) => { let value; @@ -10187,13 +14163,13 @@ diff --git a/client.js b/client.js var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { - return (0, import_react16.createElement)( -+ return (0, import_react23.createElement)( ++ return (0, import_react24.createElement)( "label", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t(label))), - (0, import_react16.createElement)(multiline ? "textarea" : "input", { -+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t(label))), -+ (0, import_react23.createElement)(multiline ? "textarea" : "input", { ++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t(label))), ++ (0, import_react24.createElement)(multiline ? "textarea" : "input", { className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, @@ -10202,7 +14178,7 @@ diff --git a/client.js b/client.js } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { - const [error, setError] = (0, import_react16.useState)(""); -+ const [error, setError] = (0, import_react23.useState)(""); ++ const [error, setError] = (0, import_react24.useState)(""); const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); @@ -10220,89 +14196,89 @@ diff --git a/client.js b/client.js }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); - return (0, import_react16.createElement)( -+ return (0, import_react23.createElement)( ++ return (0, import_react24.createElement)( "form", { className: "bt-connector-form", onSubmit: submit }, - (0, import_react16.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), - (0, import_react16.createElement)( -+ (0, import_react23.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }), ++ (0, import_react24.createElement)( "label", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Type"))), - (0, import_react16.createElement)( -+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Type"))), -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Type"))), ++ (0, import_react24.createElement)( "select", { className: "bt-input", value: draft.kind, disabled: draft.originalName !== void 0, onChange: (event) => set("kind", event.target.value) }, - ["mcp", "skill", "pack"].map((kind) => (0, import_react16.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) -+ ["mcp", "skill", "pack"].map((kind) => (0, import_react23.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) ++ ["mcp", "skill", "pack"].map((kind) => (0, import_react24.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack"))) ) ), - (0, import_react16.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), - draft.kind === "mcp" ? (0, import_react16.createElement)( -+ (0, import_react23.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), -+ draft.kind === "mcp" ? (0, import_react23.createElement)( ++ (0, import_react24.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }), ++ draft.kind === "mcp" ? (0, import_react24.createElement)( "label", { className: "bt-set-row bt-bs-row bt-bs-stack" }, - (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), - (0, import_react16.createElement)( -+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Connection"))), ++ (0, import_react24.createElement)( "select", { className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) }, - (0, import_react16.createElement)("option", { value: "stdio" }, t("Local command")), - (0, import_react16.createElement)("option", { value: "streamable-http" }, t("HTTP server")) -+ (0, import_react23.createElement)("option", { value: "stdio" }, t("Local command")), -+ (0, import_react23.createElement)("option", { value: "streamable-http" }, t("HTTP server")) ++ (0, import_react24.createElement)("option", { value: "stdio" }, t("Local command")), ++ (0, import_react24.createElement)("option", { value: "streamable-http" }, t("HTTP server")) ) ) : null, - draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react16.createElement)( -+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react23.createElement)( ++ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react24.createElement)( "div", null, - (0, import_react16.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), - (0, import_react16.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), - (0, import_react16.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ (0, import_react23.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), -+ (0, import_react23.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), -+ (0, import_react23.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ++ (0, import_react24.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }), ++ (0, import_react24.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }), ++ (0, import_react24.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ) : null, - draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react16.createElement)( -+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react23.createElement)( ++ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react24.createElement)( "div", null, - (0, import_react16.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), - (0, import_react16.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) -+ (0, import_react23.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), -+ (0, import_react23.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ++ (0, import_react24.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }), ++ (0, import_react24.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') }) ) : null, - draft.kind === "skill" ? (0, import_react16.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, - draft.kind === "pack" ? (0, import_react16.createElement)( -+ draft.kind === "skill" ? (0, import_react23.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, -+ draft.kind === "pack" ? (0, import_react23.createElement)( ++ draft.kind === "skill" ? (0, import_react24.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null, ++ draft.kind === "pack" ? (0, import_react24.createElement)( "div", { className: "bt-set-card" }, - available.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react16.createElement)( -+ available.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react23.createElement)( ++ available.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react24.createElement)( "label", { key: item.name, className: "bt-set-row bt-bs-row" }, - (0, import_react16.createElement)("span", { className: "bt-set-copy" }, (0, import_react16.createElement)("span", { className: "bt-bs-label" }, item.name)), - (0, import_react16.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) -+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, item.name)), -+ (0, import_react23.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) ++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, item.name)), ++ (0, import_react24.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) }) )) ) : null, - error ? (0, import_react16.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, - (0, import_react16.createElement)( -+ error ? (0, import_react23.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, -+ (0, import_react23.createElement)( ++ error ? (0, import_react24.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null, ++ (0, import_react24.createElement)( "div", { className: "bt-connector-form-row" }, - (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), - (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), -+ (0, import_react23.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")), ++ (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability")) ) ); } @@ -10310,32 +14286,32 @@ diff --git a/client.js b/client.js const detail = item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill"); const secondary = item.kind === "pack" ? [item.description, (item.items ?? []).join(", ") || t("No items")].filter(Boolean).join(" · ") : item.description || detail; - return (0, import_react16.createElement)( -+ return (0, import_react23.createElement)( ++ return (0, import_react24.createElement)( "div", { className: "bt-connector" }, - (0, import_react16.createElement)( -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)( "div", { className: "bt-connector-head" }, - (0, import_react16.createElement)( -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)( "span", { className: "bt-connector-text" }, - (0, import_react16.createElement)("span", { className: "bt-connector-name" }, item.name), - (0, import_react16.createElement)("span", { className: "bt-connector-status" }, secondary) -+ (0, import_react23.createElement)("span", { className: "bt-connector-name" }, item.name), -+ (0, import_react23.createElement)("span", { className: "bt-connector-status" }, secondary) ++ (0, import_react24.createElement)("span", { className: "bt-connector-name" }, item.name), ++ (0, import_react24.createElement)("span", { className: "bt-connector-status" }, secondary) ), - (0, import_react16.createElement)( -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)( "span", { className: "bt-connector-actions" }, - item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, - (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), - (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) -+ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) ++ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null, ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")), ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete")) ) ) ); @@ -10345,9 +14321,9 @@ diff --git a/client.js b/client.js - const [draft, setDraft] = (0, import_react16.useState)(null); - const [confirming, setConfirming] = (0, import_react16.useState)(""); - (0, import_react16.useEffect)(() => { -+ const [draft, setDraft] = (0, import_react23.useState)(null); -+ const [confirming, setConfirming] = (0, import_react23.useState)(""); -+ (0, import_react23.useEffect)(() => { ++ const [draft, setDraft] = (0, import_react24.useState)(null); ++ const [confirming, setConfirming] = (0, import_react24.useState)(""); ++ (0, import_react24.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); @@ -10356,26 +14332,26 @@ diff --git a/client.js b/client.js void run(actions.deleteCapability(item.name)); }; - return (0, import_react16.createElement)( -+ return (0, import_react23.createElement)( ++ return (0, import_react24.createElement)( "section", { className: "bt-set-section" }, - (0, import_react16.createElement)("h3", null, t("Shared capabilities")), - (0, import_react16.createElement)( -+ (0, import_react23.createElement)("h3", null, t("Shared capabilities")), -+ (0, import_react23.createElement)( ++ (0, import_react24.createElement)("h3", null, t("Shared capabilities")), ++ (0, import_react24.createElement)( "div", { className: "bt-actions" }, - (0, import_react16.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) -+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) ++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability")) ), - draft ? (0, import_react16.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, - (0, import_react16.createElement)( -+ draft ? (0, import_react23.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, -+ (0, import_react23.createElement)( ++ draft ? (0, import_react24.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null, ++ (0, import_react24.createElement)( "div", { className: "bt-set-card bt-connectors-card" }, - items.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-set-row" }, (0, import_react16.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react16.createElement)(CapabilityRow, { -+ items.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react23.createElement)(CapabilityRow, { ++ items.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react24.createElement)(CapabilityRow, { key: item.name, item, confirming: confirming === item.name, @@ -10384,11 +14360,11 @@ diff --git a/client.js b/client.js const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; - return (0, import_react16.createElement)( -+ return (0, import_react23.createElement)( ++ return (0, import_react24.createElement)( Card2, { title: t("Capabilities") }, - items.map((item) => (0, import_react16.createElement)(SwitchRow2, { -+ items.map((item) => (0, import_react23.createElement)(SwitchRow2, { ++ items.map((item) => (0, import_react24.createElement)(SwitchRow2, { key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, @@ -10397,7 +14373,7 @@ diff --git a/client.js b/client.js } +// src/client/connector-bots.js -+var import_react24 = require("react"); ++var import_react25 = require("react"); +function ownBots(roster) { + const me = roster?.self?.memberId; + return (roster?.bots ?? []).filter((bot) => !bot.remote && (me === void 0 || me === null || bot.ownerId === void 0 || bot.ownerId === me)); @@ -10413,31 +14389,31 @@ diff --git a/client.js b/client.js + return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length }); +} +function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) { -+ return (0, import_react24.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); ++ return (0, import_react25.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) }); +} +function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) { + const bots = ownBots(roster); -+ return (0, import_react24.createElement)( ++ return (0, import_react25.createElement)( + "div", + { className: "bt-connector-bots" }, -+ bots.length === 0 ? (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react24.createElement)( ++ bots.length === 0 ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react25.createElement)( + "div", + { key: bot.id, className: "bt-connector-bot" }, -+ (0, import_react24.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) ++ (0, import_react25.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 }) + )), -+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) ++ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it.")) + ); +} +function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { + const connected = connectedConnectors(roster); + if (connected.length === 0) return null; -+ return (0, import_react24.createElement)( ++ return (0, import_react25.createElement)( + Card2, + { title: t("Connectors") }, -+ connected.map((connector) => (0, import_react24.createElement)( ++ connected.map((connector) => (0, import_react25.createElement)( + "div", + { key: connector.id, className: "bt-connector-bot" }, -+ (0, import_react24.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) ++ (0, import_react25.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) + )) + ); +} @@ -10445,7 +14421,7 @@ diff --git a/client.js b/client.js // src/client/memory-panel.js -var import_react17 = require("react"); -var POLL_MS2 = 1500; -+var import_react25 = require("react"); ++var import_react26 = require("react"); +var POLL_MS6 = 1500; var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); @@ -10456,8 +14432,8 @@ diff --git a/client.js b/client.js function useMemoryCount(botId, actions) { - const [count, setCount] = (0, import_react17.useState)(counts.get(botId) ?? null); - (0, import_react17.useEffect)(() => { -+ const [count, setCount] = (0, import_react25.useState)(counts.get(botId) ?? null); -+ (0, import_react25.useEffect)(() => { ++ const [count, setCount] = (0, import_react26.useState)(counts.get(botId) ?? null); ++ (0, import_react26.useEffect)(() => { const listener = () => setCount(counts.get(botId) ?? null); countListeners.add(listener); listener(); @@ -10472,21 +14448,21 @@ diff --git a/client.js b/client.js const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", null, - (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Memory")), -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Memory")), ++ (0, import_react26.createElement)( "button", { type: "button", className: "bt-mem-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, - (0, import_react17.createElement)(MemoryIcon), - (0, import_react17.createElement)("span", { className: "bt-mem-row-copy" }, label), - (0, import_react17.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react17.createElement)(ChevronRightIcon)) -+ (0, import_react25.createElement)(MemoryIcon), -+ (0, import_react25.createElement)("span", { className: "bt-mem-row-copy" }, label), -+ (0, import_react25.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react25.createElement)(ChevronRightIcon)) ++ (0, import_react26.createElement)(MemoryIcon), ++ (0, import_react26.createElement)("span", { className: "bt-mem-row-copy" }, label), ++ (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon)) ) ); } @@ -10495,63 +14471,63 @@ diff --git a/client.js b/client.js } function Skeleton({ label }) { - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", { className: "bt-mem-skel", "aria-busy": true }, - [0, 1].map((block) => (0, import_react17.createElement)( -+ [0, 1].map((block) => (0, import_react25.createElement)( ++ [0, 1].map((block) => (0, import_react26.createElement)( "div", { key: block, className: "bt-mem-skel-block" }, - (0, import_react17.createElement)("span", { className: "bt-mem-skel-head" }), - (0, import_react17.createElement)("span", null), - (0, import_react17.createElement)("span", null), - (0, import_react17.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) -+ (0, import_react25.createElement)("span", { className: "bt-mem-skel-head" }), -+ (0, import_react25.createElement)("span", null), -+ (0, import_react25.createElement)("span", null), -+ (0, import_react25.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) ++ (0, import_react26.createElement)("span", { className: "bt-mem-skel-head" }), ++ (0, import_react26.createElement)("span", null), ++ (0, import_react26.createElement)("span", null), ++ (0, import_react26.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } }) )), - label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null -+ label ? (0, import_react25.createElement)("div", { className: "bt-mem-skel-label" }, label) : null ++ label ? (0, import_react26.createElement)("div", { className: "bt-mem-skel-label" }, label) : null ); } function Summary({ bot, view, regenerate }) { if (view.entries.length === 0) { - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", { className: "bt-mem-empty" }, - (0, import_react17.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react17.createElement)(MemoryIcon)), - (0, import_react17.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), - (0, import_react17.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) -+ (0, import_react25.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react25.createElement)(MemoryIcon)), -+ (0, import_react25.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), -+ (0, import_react25.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) ++ (0, import_react26.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react26.createElement)(MemoryIcon)), ++ (0, import_react26.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })), ++ (0, import_react26.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name })) ); } - const failed = view.error && !view.summarizing ? (0, import_react17.createElement)( -+ const failed = view.error && !view.summarizing ? (0, import_react25.createElement)( ++ const failed = view.error && !view.summarizing ? (0, import_react26.createElement)( "div", { className: "bt-mem-failed", role: "alert" }, - (0, import_react17.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), - (0, import_react17.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) -+ (0, import_react25.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) ++ (0, import_react26.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })), ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again")) ) : null; - if (!view.summary) return (0, import_react17.createElement)(import_react17.Fragment, null, failed, failed ? null : (0, import_react17.createElement)(Skeleton, { label: t("Writing the summary…") })); - return (0, import_react17.createElement)( - import_react17.Fragment, -+ if (!view.summary) return (0, import_react25.createElement)(import_react25.Fragment, null, failed, failed ? null : (0, import_react25.createElement)(Skeleton, { label: t("Writing the summary…") })); -+ return (0, import_react25.createElement)( -+ import_react25.Fragment, ++ if (!view.summary) return (0, import_react26.createElement)(import_react26.Fragment, null, failed, failed ? null : (0, import_react26.createElement)(Skeleton, { label: t("Writing the summary…") })); ++ return (0, import_react26.createElement)( ++ import_react26.Fragment, null, failed, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { key: view.summary.at, className: "bt-mem-summary", "data-stale": !view.fresh || void 0 }, - summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react17.createElement)("h3", { key: index }, block.text) : (0, import_react17.createElement)("p", { key: index }, block.text)) -+ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react25.createElement)("h3", { key: index }, block.text) : (0, import_react25.createElement)("p", { key: index }, block.text)) ++ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react26.createElement)("h3", { key: index }, block.text) : (0, import_react26.createElement)("p", { key: index }, block.text)) ) ); } @@ -10562,9 +14538,9 @@ diff --git a/client.js b/client.js - const [confirming, setConfirming] = (0, import_react17.useState)(false); - const [busy, setBusy] = (0, import_react17.useState)(false); - (0, import_react17.useEffect)(() => { -+ const [confirming, setConfirming] = (0, import_react25.useState)(false); -+ const [busy, setBusy] = (0, import_react25.useState)(false); -+ (0, import_react25.useEffect)(() => { ++ const [confirming, setConfirming] = (0, import_react26.useState)(false); ++ const [busy, setBusy] = (0, import_react26.useState)(false); ++ (0, import_react26.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); @@ -10573,20 +14549,20 @@ diff --git a/client.js b/client.js if (!await remove(entry)) setBusy(false); }; - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "li", { className: "bt-mem-entry", "data-busy": busy || void 0 }, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-entry-copy" }, - (0, import_react17.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), - meta ? (0, import_react17.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null -+ (0, import_react25.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), -+ meta ? (0, import_react25.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null ++ (0, import_react26.createElement)("span", { className: "bt-mem-entry-text" }, entry.text), ++ meta ? (0, import_react26.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null ), - (0, import_react17.createElement)("button", { -+ (0, import_react25.createElement)("button", { ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, @@ -10595,7 +14571,7 @@ diff --git a/client.js b/client.js title: confirming ? void 0 : t("Remove from memory"), onClick: click - }, confirming ? t("Remove") : (0, import_react17.createElement)(TrashIcon)) -+ }, confirming ? t("Remove") : (0, import_react25.createElement)(TrashIcon)) ++ }, confirming ? t("Remove") : (0, import_react26.createElement)(TrashIcon)) ); } function Entries({ bot, view, remove }) { @@ -10604,35 +14580,35 @@ diff --git a/client.js b/client.js { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", { className: "bt-mem-entries" }, - sections.map((section) => (0, import_react17.createElement)( -+ sections.map((section) => (0, import_react25.createElement)( ++ sections.map((section) => (0, import_react26.createElement)( "section", { key: section.scope, className: "bt-mem-section" }, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-section-head" }, - (0, import_react17.createElement)("h3", null, section.title), - section.hint ? (0, import_react17.createElement)("span", null, section.hint) : null, - (0, import_react17.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) -+ (0, import_react25.createElement)("h3", null, section.title), -+ section.hint ? (0, import_react25.createElement)("span", null, section.hint) : null, -+ (0, import_react25.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) ++ (0, import_react26.createElement)("h3", null, section.title), ++ section.hint ? (0, import_react26.createElement)("span", null, section.hint) : null, ++ (0, import_react26.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) })) ), - groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react17.createElement)( - import_react17.Fragment, -+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react25.createElement)( -+ import_react25.Fragment, ++ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react26.createElement)( ++ import_react26.Fragment, { key: group.key }, - group.topic ? (0, import_react17.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, - (0, import_react17.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react17.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) - )) : (0, import_react17.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) -+ group.topic ? (0, import_react25.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, -+ (0, import_react25.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react25.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) -+ )) : (0, import_react25.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) ++ group.topic ? (0, import_react26.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null, ++ (0, import_react26.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react26.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove }))) ++ )) : (0, import_react26.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet")) )) ); } @@ -10641,33 +14617,33 @@ diff --git a/client.js b/client.js } function Reply({ answer, dismiss }) { - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", { className: "bt-mem-reply", role: "status" }, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-reply-head" }, - (0, import_react17.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), - answer.pending ? null : (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react17.createElement)(CloseIcon)) -+ (0, import_react25.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), -+ answer.pending ? null : (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react25.createElement)(CloseIcon)) ++ (0, import_react26.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked), ++ answer.pending ? null : (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react26.createElement)(CloseIcon)) ), - answer.pending ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react17.createElement)( - import_react17.Fragment, -+ answer.pending ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react25.createElement)( -+ import_react25.Fragment, ++ answer.pending ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react26.createElement)( ++ import_react26.Fragment, null, - answer.reply ? (0, import_react17.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, - answer.changes.length > 0 ? (0, import_react17.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react17.createElement)( -+ answer.reply ? (0, import_react25.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, -+ answer.changes.length > 0 ? (0, import_react25.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react25.createElement)( ++ answer.reply ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null, ++ answer.changes.length > 0 ? (0, import_react26.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react26.createElement)( "li", { key: index, "data-ok": change.ok || void 0 }, - change.ok ? (0, import_react17.createElement)(CheckIcon) : (0, import_react17.createElement)(CloseIcon), - (0, import_react17.createElement)("span", null, changeLine(change)) -+ change.ok ? (0, import_react25.createElement)(CheckIcon) : (0, import_react25.createElement)(CloseIcon), -+ (0, import_react25.createElement)("span", null, changeLine(change)) ++ change.ok ? (0, import_react26.createElement)(CheckIcon) : (0, import_react26.createElement)(CloseIcon), ++ (0, import_react26.createElement)("span", null, changeLine(change)) ))) : null ) ); @@ -10679,18 +14655,18 @@ diff --git a/client.js b/client.js - const latest = (0, import_react17.useRef)(0); - const alive = (0, import_react17.useRef)(true); - const show = (0, import_react17.useCallback)((next) => { -+ const [view, setView] = (0, import_react25.useState)(null); -+ const [error, setError] = (0, import_react25.useState)(""); -+ const timer = (0, import_react25.useRef)(0); -+ const latest = (0, import_react25.useRef)(0); -+ const alive = (0, import_react25.useRef)(true); -+ const show = (0, import_react25.useCallback)((next) => { ++ const [view, setView] = (0, import_react26.useState)(null); ++ const [error, setError] = (0, import_react26.useState)(""); ++ const timer = (0, import_react26.useRef)(0); ++ const latest = (0, import_react26.useRef)(0); ++ const alive = (0, import_react26.useRef)(true); ++ const show = (0, import_react26.useCallback)((next) => { setView(next); setError(""); noteCount(botId, next.entries.length); }, [botId]); - const load = (0, import_react17.useCallback)(async (flags = {}) => { -+ const load = (0, import_react25.useCallback)(async (flags = {}) => { ++ const load = (0, import_react26.useCallback)(async (flags = {}) => { clearTimeout(timer.current); const ticket = ++latest.current; try { @@ -10704,7 +14680,7 @@ diff --git a/client.js b/client.js } }, [botId]); - (0, import_react17.useEffect)(() => { -+ (0, import_react25.useEffect)(() => { ++ (0, import_react26.useEffect)(() => { alive.current = true; void load({ summarize: true }); return () => { @@ -10716,8 +14692,8 @@ diff --git a/client.js b/client.js - const [now, setNow] = (0, import_react17.useState)(Date.now()); - (0, import_react17.useEffect)(() => { +function useNow3(every) { -+ const [now, setNow] = (0, import_react25.useState)(Date.now()); -+ (0, import_react25.useEffect)(() => { ++ const [now, setNow] = (0, import_react26.useState)(Date.now()); ++ (0, import_react26.useEffect)(() => { const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); @@ -10727,8 +14703,8 @@ diff --git a/client.js b/client.js const { view, error, load, show } = useMemoryView(bot.id, actions); - const [page, setPage] = (0, import_react17.useState)("summary"); - const [menu, setMenu] = (0, import_react17.useState)(false); -+ const [page, setPage] = (0, import_react25.useState)("summary"); -+ const [menu, setMenu] = (0, import_react25.useState)(false); ++ const [page, setPage] = (0, import_react26.useState)("summary"); ++ const [menu, setMenu] = (0, import_react26.useState)(false); const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); - const [text, setText] = (0, import_react17.useState)(""); - const [answer, setAnswer] = (0, import_react17.useState)(null); @@ -10737,16 +14713,16 @@ diff --git a/client.js b/client.js - const inputRef = (0, import_react17.useRef)(null); - const now = useNow(3e4); - const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); -+ const [text, setText] = (0, import_react25.useState)(""); -+ const [answer, setAnswer] = (0, import_react25.useState)(null); -+ const menuRef = (0, import_react25.useRef)(null); -+ const dotsRef = (0, import_react25.useRef)(null); -+ const inputRef = (0, import_react25.useRef)(null); ++ const [text, setText] = (0, import_react26.useState)(""); ++ const [answer, setAnswer] = (0, import_react26.useState)(null); ++ const menuRef = (0, import_react26.useRef)(null); ++ const dotsRef = (0, import_react26.useRef)(null); ++ const inputRef = (0, import_react26.useRef)(null); + const now = useNow3(3e4); -+ const close = (0, import_react25.useCallback)(() => actions.closeMemory(), []); ++ const close = (0, import_react26.useCallback)(() => actions.closeMemory(), []); useEscape(close); - (0, import_react17.useEffect)(() => { -+ (0, import_react25.useEffect)(() => { ++ (0, import_react26.useEffect)(() => { if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; @@ -10755,17 +14731,17 @@ diff --git a/client.js b/client.js const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; const busy = answer?.pending === true; - return (0, import_react17.createElement)( -+ return (0, import_react25.createElement)( ++ return (0, import_react26.createElement)( "div", { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, - (0, import_react17.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), ++ (0, import_react26.createElement)( "div", { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-head" }, - page === "entries" ? (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react17.createElement)(ChevronLeftIcon)) : null, @@ -10775,54 +14751,54 @@ diff --git a/client.js b/client.js - (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)), - (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)), - menuShown ? (0, import_react17.createElement)( -+ page === "entries" ? (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react25.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react25.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -+ (0, import_react25.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -+ (0, import_react25.createElement)("span", { className: "bt-mem-head-gap" }), -+ (0, import_react25.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react25.createElement)(DotsIcon)), -+ (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react25.createElement)(CloseIcon)), -+ menuShown ? (0, import_react25.createElement)( ++ page === "entries" ? (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react26.createElement)(ChevronLeftIcon)) : null, ++ (0, import_react26.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), ++ (0, import_react26.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), ++ (0, import_react26.createElement)("span", { className: "bt-mem-head-gap" }), ++ (0, import_react26.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react26.createElement)(DotsIcon)), ++ (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react26.createElement)(CloseIcon)), ++ menuShown ? (0, import_react26.createElement)( "div", { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "button", { type: "button", role: "menuitem", onClick: openEntries }, - (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("All entries")), - (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, String(count)) -+ (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -+ (0, import_react25.createElement)("span", { className: "bt-menu-hint" }, String(count)) ++ (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("All entries")), ++ (0, import_react26.createElement)("span", { className: "bt-menu-hint" }, String(count)) ), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "button", { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, - (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) -+ (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) ++ (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) ) ) : null ), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-body" }, - error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, - view === null ? error ? null : (0, import_react17.createElement)(Skeleton) : page === "entries" ? (0, import_react17.createElement)(Entries, { bot, view, remove }) : (0, import_react17.createElement)(Summary, { bot, view, regenerate }) -+ error ? (0, import_react25.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -+ view === null ? error ? null : (0, import_react25.createElement)(Skeleton) : page === "entries" ? (0, import_react25.createElement)(Entries, { bot, view, remove }) : (0, import_react25.createElement)(Summary, { bot, view, regenerate }) ++ error ? (0, import_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, ++ view === null ? error ? null : (0, import_react26.createElement)(Skeleton) : page === "entries" ? (0, import_react26.createElement)(Entries, { bot, view, remove }) : (0, import_react26.createElement)(Summary, { bot, view, regenerate }) ), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "div", { className: "bt-mem-foot" }, - answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, - (0, import_react17.createElement)( -+ answer ? (0, import_react25.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -+ (0, import_react25.createElement)( ++ answer ? (0, import_react26.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, ++ (0, import_react26.createElement)( "form", { className: "bt-mem-ask", onSubmit: ask }, - (0, import_react17.createElement)("input", { -+ (0, import_react25.createElement)("input", { ++ (0, import_react26.createElement)("input", { ref: inputRef, className: "bt-mem-input", value: text, @@ -10831,11 +14807,11 @@ diff --git a/client.js b/client.js onChange: (event) => setText(event.target.value) }), - (0, import_react17.createElement)( -+ (0, import_react25.createElement)( ++ (0, import_react26.createElement)( "button", { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, - busy ? (0, import_react17.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react17.createElement)(ArrowUpIcon) -+ busy ? (0, import_react25.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react25.createElement)(ArrowUpIcon) ++ busy ? (0, import_react26.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react26.createElement)(ArrowUpIcon) ) ) ) @@ -10844,7 +14820,7 @@ diff --git a/client.js b/client.js } +// src/client/standing-approvals.js -+var import_react26 = require("react"); ++var import_react27 = require("react"); +var standingOf = (target, roster) => { + const me = roster?.self?.memberId; + return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me); @@ -10853,19 +14829,19 @@ diff --git a/client.js b/client.js + const grants = scope === "room" ? standingOf(target, roster) : target?.standingApprovals ?? []; + if (grants.length === 0) return null; + const hint = scope === "room" ? t("Your Bots in this group do these without asking.") : t("This Bot does these without asking."); -+ return (0, import_react26.createElement)( ++ return (0, import_react27.createElement)( + Card2, + { title: t("Always allowed") }, -+ grants.map((grant) => (0, import_react26.createElement)( ++ grants.map((grant) => (0, import_react27.createElement)( + "div", + { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind }, -+ (0, import_react26.createElement)( ++ (0, import_react27.createElement)( + "span", + { className: "bt-set-copy" }, -+ (0, import_react26.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), -+ (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) ++ (0, import_react27.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind), ++ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) + ), -+ (0, import_react26.createElement)("button", { ++ (0, import_react27.createElement)("button", { + type: "button", + className: "bt-soft bt-soft-sm", + "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }), @@ -10877,7 +14853,7 @@ diff --git a/client.js b/client.js + // src/client/bot-settings.js -var import_react18 = require("react"); -+var import_react27 = require("react"); ++var import_react28 = require("react"); var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); @@ -10887,8 +14863,8 @@ diff --git a/client.js b/client.js function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { - const [draft, setDraft] = (0, import_react18.useState)(null); - const reverting = (0, import_react18.useRef)(false); -+ const [draft, setDraft] = (0, import_react27.useState)(null); -+ const reverting = (0, import_react27.useRef)(false); ++ const [draft, setDraft] = (0, import_react28.useState)(null); ++ const reverting = (0, import_react28.useRef)(false); const commit = () => { const next = draft?.trim(); setDraft(null); @@ -10897,7 +14873,7 @@ diff --git a/client.js b/client.js void onSave(next); }; - return (0, import_react18.createElement)(multiline ? "textarea" : "input", { -+ return (0, import_react27.createElement)(multiline ? "textarea" : "input", { ++ return (0, import_react28.createElement)(multiline ? "textarea" : "input", { className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, @@ -10906,37 +14882,37 @@ diff --git a/client.js b/client.js } function Row({ label, hint, stack = false, children }) { - return (0, import_react18.createElement)( -+ return (0, import_react27.createElement)( ++ return (0, import_react28.createElement)( "label", { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, - (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), children ); } function Card({ title, children }) { - return (0, import_react18.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react18.createElement)("h3", null, title) : null, (0, import_react18.createElement)("div", { className: "bt-set-card" }, children)); -+ return (0, import_react27.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react27.createElement)("h3", null, title) : null, (0, import_react27.createElement)("div", { className: "bt-set-card" }, children)); ++ return (0, import_react28.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react28.createElement)("h3", null, title) : null, (0, import_react28.createElement)("div", { className: "bt-set-card" }, children)); } function Switch({ on, label, disabled, onChange }) { - return (0, import_react18.createElement)( -+ return (0, import_react27.createElement)( ++ return (0, import_react28.createElement)( "button", { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, - (0, import_react18.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) -+ (0, import_react27.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ++ (0, import_react28.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ); } -function SwitchRow({ label, hint, on, disabled, onChange }) { - return (0, import_react18.createElement)( +function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { -+ return (0, import_react27.createElement)( ++ return (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" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), - (0, import_react18.createElement)(Switch, { on, label, disabled, onChange }) -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ (0, import_react27.createElement)(Switch, { on, label, disabled, onChange }) ++ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ (0, import_react28.createElement)(Switch, { on, label, disabled, onChange }) ); } function modelHint(bot, roster) { @@ -10945,13 +14921,13 @@ diff --git a/client.js b/client.js } function ShapeRing({ shape, size }) { - const maskId = (0, import_react18.useRef)(""); -+ const maskId = (0, import_react27.useRef)(""); ++ const maskId = (0, import_react28.useRef)(""); if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; const def = shapeOf(shape); const unit = def.size / size; const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; - return (0, import_react18.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); -+ return (0, import_react27.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); ++ return (0, import_react28.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); } function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; @@ -10960,11 +14936,11 @@ diff --git a/client.js b/client.js for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); - return (0, import_react18.createElement)( - import_react18.Fragment, -+ return (0, import_react27.createElement)( -+ import_react27.Fragment, ++ return (0, import_react28.createElement)( ++ import_react28.Fragment, null, - (0, import_react18.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react18.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react18.createElement)("button", { -+ (0, import_react27.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react27.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react27.createElement)("button", { ++ (0, import_react28.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react28.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react28.createElement)("button", { key: id, type: "button", className: "bt-shape", @@ -10974,8 +14950,8 @@ diff --git a/client.js b/client.js onClick: () => onShape(id) - }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), - (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", { -+ }, (0, import_react27.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react27.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -+ (0, import_react27.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react27.createElement)("button", { ++ }, (0, import_react28.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react28.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), ++ (0, import_react28.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react28.createElement)("button", { key: color, type: "button", className: "bt-color", @@ -10984,7 +14960,7 @@ diff --git a/client.js b/client.js "aria-pressed": look.color === color, onClick: () => onColor(color) - }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } })))) -+ }, (0, import_react27.createElement)("span", { style: { background: paintCss(color) } })))) ++ }, (0, import_react28.createElement)("span", { style: { background: paintCss(color) } })))) ); } function AvatarEditor({ bot, actions, run, onClose }) { @@ -10993,15 +14969,15 @@ diff --git a/client.js b/client.js - const [over, setOver] = (0, import_react18.useState)(false); - const ref = (0, import_react18.useRef)(null); - const fileRef = (0, import_react18.useRef)(null); -+ const [tab, setTab] = (0, import_react27.useState)(look.image ? "upload" : "bot"); -+ const [over, setOver] = (0, import_react27.useState)(false); -+ const ref = (0, import_react27.useRef)(null); -+ const fileRef = (0, import_react27.useRef)(null); ++ const [tab, setTab] = (0, import_react28.useState)(look.image ? "upload" : "bot"); ++ const [over, setOver] = (0, import_react28.useState)(false); ++ const ref = (0, import_react28.useRef)(null); ++ const fileRef = (0, import_react28.useRef)(null); const upload = (file) => { if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); }; - (0, import_react18.useEffect)(() => { -+ (0, import_react27.useEffect)(() => { ++ (0, import_react28.useEffect)(() => { const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); @@ -11010,7 +14986,7 @@ diff --git a/client.js b/client.js }; }, [onClose]); - (0, import_react18.useEffect)(() => { -+ (0, import_react27.useEffect)(() => { ++ (0, import_react28.useEffect)(() => { if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); @@ -11019,24 +14995,24 @@ diff --git a/client.js b/client.js const hasPhoto = Boolean(look.image); const glide = useGlide(tab); - return (0, import_react18.createElement)( -+ return (0, import_react27.createElement)( ++ return (0, import_react28.createElement)( "div", { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, - (0, import_react18.createElement)( -+ (0, import_react27.createElement)( ++ (0, import_react28.createElement)( "div", { className: "bt-avedit-top" }, - (0, import_react18.createElement)( -+ (0, import_react27.createElement)( ++ (0, import_react28.createElement)( "div", { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, - (0, import_react18.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react18.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) -+ (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react27.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) ++ (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react28.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) ), - (0, import_react18.createElement)("button", { -+ (0, import_react27.createElement)("button", { ++ (0, import_react28.createElement)("button", { type: "button", className: "bt-avedit-tab bt-avedit-reset", title: t("Reset character to default"), @@ -11044,14 +15020,14 @@ diff --git a/client.js b/client.js }, t("Reset")) ), - tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, { -+ tab === "bot" ? (0, import_react27.createElement)(CharacterGrid, { ++ tab === "bot" ? (0, import_react28.createElement)(CharacterGrid, { look, hasPhoto, onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), onColor: (color) => run(actions.updateBot(bot.id, { color })) }) : null, - tab === "upload" ? (0, import_react18.createElement)( -+ tab === "upload" ? (0, import_react27.createElement)( ++ tab === "upload" ? (0, import_react28.createElement)( "div", { className: "bt-dropzone", @@ -11062,37 +15038,37 @@ diff --git a/client.js b/client.js - hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null, - (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")), - (0, import_react18.createElement)( -+ hasPhoto ? (0, import_react27.createElement)("img", { src: look.image, alt: "" }) : null, -+ (0, import_react27.createElement)("div", null, t("Drag, drop, or paste an image")), -+ (0, import_react27.createElement)( ++ hasPhoto ? (0, import_react28.createElement)("img", { src: look.image, alt: "" }) : null, ++ (0, import_react28.createElement)("div", null, t("Drag, drop, or paste an image")), ++ (0, import_react28.createElement)( "div", { className: "bt-actions", style: { justifyContent: "center" } }, - (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), - hasPhoto ? (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null -+ (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -+ hasPhoto ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null ++ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), ++ hasPhoto ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null ), - (0, import_react18.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ (0, import_react27.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { ++ (0, import_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { const file = event.target.files?.[0]; event.target.value = ""; upload(file); -@@ -7982,26 +7982,27 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7982,178 +7982,825 @@ function AvatarEditor({ bot, actions, run, onClose }) { function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); - return (0, import_react18.createElement)( -+ return (0, import_react27.createElement)( ++ return (0, import_react28.createElement)( Card, { title: t("Memory") }, - (0, import_react18.createElement)( -+ (0, import_react27.createElement)( ++ (0, import_react28.createElement)( "button", { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, - (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label)), - (0, import_react18.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react18.createElement)(ChevronRightIcon)) -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, label)), -+ (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon)) ++ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label)), ++ (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon)) ) ); } @@ -11101,10 +15077,10 @@ diff --git a/client.js b/client.js - const [confirming, setConfirming] = (0, import_react18.useState)(false); - const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []); - (0, import_react18.useEffect)(() => { -+ const [editing, setEditing] = (0, import_react27.useState)(false); -+ const [confirming, setConfirming] = (0, import_react27.useState)(false); -+ const closeEditor = (0, import_react27.useCallback)(() => setEditing(false), []); -+ (0, import_react27.useEffect)(() => { ++ const [editing, setEditing] = (0, import_react28.useState)(false); ++ const [confirming, setConfirming] = (0, import_react28.useState)(false); ++ const closeEditor = (0, import_react28.useCallback)(() => setEditing(false), []); ++ (0, import_react28.useEffect)(() => { if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 4e3); return () => clearTimeout(timer); @@ -11113,186 +15089,165 @@ diff --git a/client.js b/client.js const isMainBot = isMainOf(roster, bot.id); const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; const isTile = roster.mainBotId === bot.id; -@@ -8011,112 +8011,116 @@ function BotSettingsForm({ bot, roster, actions, run }) { - if (confirming) void run(actions.deleteBot(bot.id)); - else setConfirming(true); - }; -- (0, import_react18.useEffect)(() => { -+ (0, import_react27.useEffect)(() => { - void actions.refreshModels?.(); - }, [bot.id]); -- return (0, import_react18.createElement)( -+ return (0, import_react27.createElement)( - "div", - { className: "bt-bs" }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-bs-hero" }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-avatar-anchor" }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "button", - { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), -- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) -+ (0, import_react27.createElement)(BotMark, { bot, size: 64, live: true }), -+ (0, import_react27.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react27.createElement)(PencilIcon)) - ), -- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null -+ editing ? (0, import_react27.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null - ), -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-bs-who" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) -+ (0, import_react27.createElement)("span", { className: "bt-bs-name" }, bot.name), -+ (0, import_react27.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) - ) - ), -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - Card, - { title: t("Profile") }, -- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) -+ (0, import_react27.createElement)(Row, { label: t("Name") }, (0, import_react27.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -+ (0, import_react27.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react27.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) - ), -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - Card, - { title: t("Model") }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) -- ) -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -+ (0, import_react27.createElement)(ModelMenu, { bot, roster, actions, run }) + const save = (patch) => run(actions.updateBot(bot.id, patch)); + const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id))); + const remove = () => { +- if (confirming) void run(actions.deleteBot(bot.id)); +- else setConfirming(true); ++ if (confirming) void run(actions.deleteBot(bot.id)); ++ else setConfirming(true); ++ }; ++ (0, import_react28.useEffect)(() => { ++ void actions.refreshModels?.(); ++ }, [bot.id]); ++ return (0, import_react28.createElement)( ++ "div", ++ { className: "bt-bs" }, ++ (0, import_react28.createElement)( ++ "div", ++ { className: "bt-bs-hero" }, ++ (0, import_react28.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_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_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ++ ), ++ (0, import_react28.createElement)( ++ "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_react28.createElement)( ++ 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_react28.createElement)( ++ Card, ++ { title: t("Model") }, ++ (0, import_react28.createElement)( ++ "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_react27.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null - ), -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - Card, - { title: t("Working folder") }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -+ (0, import_react27.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) - ) - ), -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - Card, - { title: t("Instructions") }, -- (0, import_react18.createElement)( -+ (0, import_react27.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_react27.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)( ++ bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null ++ ), ++ (0, import_react28.createElement)( ++ Card, ++ { title: t("Working folder") }, ++ (0, import_react28.createElement)( ++ "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_react28.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_react28.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) ++ ) ++ ), + // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). -+ (0, import_react27.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react27.createElement)(Card, { title: t("Sharing") }, children) }), -+ (0, import_react27.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -+ (0, import_react27.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), -+ roster.memory === false ? null : (0, import_react27.createElement)(MemoryCard, { bot, actions }), -+ (0, import_react27.createElement)(BotSchedules, { bot, roster, actions }), -+ (0, import_react27.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), -+ (0, import_react27.createElement)( - Card, - { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( -+ onlyMain ? (0, import_react27.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( -+ (0, import_react27.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) -+ (0, import_react27.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_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -+ ) : (0, import_react27.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)( -+ isTile ? null : (0, import_react27.createElement)( - Card, - { title: t("Sidebar") }, -- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) -+ (0, import_react27.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_react27.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_react27.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( ++ (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 }), ++ (0, import_react28.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }), ++ roster.memory === false ? null : (0, import_react28.createElement)(MemoryCard, { bot, actions }), ++ (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_react28.createElement)( ++ "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" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ ), ++ (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_react28.createElement)( ++ 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_react28.createElement)( ++ Card, + { title: t("More") }, -+ (0, import_react27.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)) -+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -+ (0, import_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon)) - ), -- isMainBot ? null : (0, import_react18.createElement)( -+ isMainBot ? null : (0, import_react27.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( -+ (0, import_react27.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_react27.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -+ (0, import_react27.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_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon)) - ) - ) - ); -@@ -8133,6 +8133,7 @@ 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%} ++ (0, import_react28.createElement)( ++ "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_react28.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_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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) ++ ) ++ ) ++ ); ++} ++var BOT_SETTINGS_CSS = ` ++.bt-bs{display:flex;flex-direction:column;gap:18px} ++.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} ++.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} ++.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-bs .bt-avedit{left:0;transform:none} ++.bt-bs-row{min-height:48px} ++label.bt-bs-row{cursor:text} ++.bt-bs-row>.bt-set-copy{flex:1 1 auto} ++.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} ++.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +${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} -@@ -8155,6 +8155,647 @@ label.bt-bs-row{cursor:text} - @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}} ++`; ++ +// src/client/group-settings.js -+var import_react28 = require("react"); ++var import_react29 = require("react"); +var ROOM_MODES = [ + ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], + ["everyone", "Everyone", "Every member answers in turn, admin first."] @@ -11324,9 +15279,9 @@ diff --git a/client.js b/client.js + for (const seat of person?.seats ?? []) { + const bot = faceSeatBot(seat, roster); + if (!bot) continue; -+ marks.push((0, import_react28.createElement)(BotMark, { key: seat.key, bot, size, live: false })); ++ marks.push((0, import_react29.createElement)(BotMark, { key: seat.key, bot, size, live: false })); + } -+ if (marks.length === 0) return [(0, import_react28.createElement)(PersonMark, { key: "person", size, name: person?.name })]; ++ if (marks.length === 0) return [(0, import_react29.createElement)(PersonMark, { key: "person", size, name: person?.name })]; + return marks; +} +function faceRoomCount(room2, roster) { @@ -11340,31 +15295,31 @@ diff --git a/client.js b/client.js +var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead)); +function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) { + const bots = person.seats.flatMap((seat, index) => { -+ const name = seat.local && onOpenBot ? (0, import_react28.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react28.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); -+ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react28.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; -+ return index === 0 ? marked : [(0, import_react28.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; ++ const name = seat.local && onOpenBot ? (0, import_react29.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react29.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); ++ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react29.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; ++ return index === 0 ? marked : [(0, import_react29.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; + }); -+ return (0, import_react28.createElement)( ++ return (0, import_react29.createElement)( + "div", + { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "span", + { className: "bt-gs-who bt-face-person" }, -+ (0, import_react28.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)), ++ (0, import_react29.createElement)( + "span", + { className: "bt-face-person-text" }, -+ (0, import_react28.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), -+ (0, import_react28.createElement)("span", { className: "bt-face-bots" }, bots) ++ (0, import_react29.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)), ++ (0, import_react29.createElement)("span", { className: "bt-face-bots" }, bots) + ) + ) + ); +} +function FaceLead({ room: room2, roster, pick: pick2 }) { + const lead = room2.face?.lead; -+ const [open, setOpen] = (0, import_react28.useState)(false); -+ const [busy, setBusy] = (0, import_react28.useState)(false); -+ const [error, setError] = (0, import_react28.useState)(""); ++ const [open, setOpen] = (0, import_react29.useState)(false); ++ const [busy, setBusy] = (0, import_react29.useState)(false); ++ const [error, setError] = (0, import_react29.useState)(""); + const seats = faceSeats(room2, roster); + const current = seats.find((seat) => isLeadSeat(seat, lead?.handle)); + if (!current) return null; @@ -11381,30 +15336,30 @@ diff --git a/client.js b/client.js + setBusy(false); + } + }; -+ return (0, import_react28.createElement)( ++ return (0, import_react29.createElement)( + "div", + { className: "bt-face-lead", "data-flybot": "face-lead" }, -+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Room lead")), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Room lead")), ++ (0, import_react29.createElement)( + "div", + { className: "bt-set-row" }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "span", + { className: "bt-set-copy" }, -+ (0, import_react28.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), -+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) ++ (0, import_react29.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)), ++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined.")) + ), -+ canPick ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null ++ canPick ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null + ), -+ open && canPick ? seats.map((seat) => (0, import_react28.createElement)( ++ open && canPick ? seats.map((seat) => (0, import_react29.createElement)( + "div", + { key: seat.key, className: "bt-set-row" }, -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), -+ seat === current ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)), ++ seat === current ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead")) + )) : null, -+ open && canPick && lead.picked ? (0, import_react28.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, -+ canPick || !lead.supported ? null : (0, import_react28.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), -+ error ? (0, import_react28.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null ++ open && canPick && lead.picked ? (0, import_react29.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, ++ canPick || !lead.supported ? null : (0, import_react29.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), ++ error ? (0, import_react29.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null + ); +} +function seatedCount(room2) { @@ -11420,9 +15375,9 @@ diff --git a/client.js b/client.js + return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); +} +function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { -+ const [adding, setAdding] = (0, import_react28.useState)(false); -+ const [confirming, setConfirming] = (0, import_react28.useState)(false); -+ (0, import_react28.useEffect)(() => { ++ const [adding, setAdding] = (0, import_react29.useState)(false); ++ const [confirming, setConfirming] = (0, import_react29.useState)(false); ++ (0, import_react29.useEffect)(() => { + if (!confirming) return void 0; + const timer = setTimeout(() => setConfirming(false), 4e3); + return () => clearTimeout(timer); @@ -11459,42 +15414,70 @@ diff --git a/client.js b/client.js + void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { + if (done) onDeleted?.(); + }); -+ }; -+ return (0, import_react28.createElement)( -+ "div", -+ { className: "bt-bs" }, -+ (0, import_react28.createElement)( -+ "div", -+ { className: "bt-bs-hero" }, -+ (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ (0, import_react28.createElement)( -+ "div", -+ { className: "bt-bs-who" }, -+ (0, import_react28.createElement)("span", { className: "bt-bs-name" }, room2.name), -+ (0, import_react28.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) -+ ) -+ ), -+ (0, import_react28.createElement)( -+ Card, -+ { title: t("Profile") }, -+ (0, import_react28.createElement)( + }; +- (0, import_react18.useEffect)(() => { +- void actions.refreshModels?.(); +- }, [bot.id]); +- return (0, import_react18.createElement)( ++ return (0, import_react29.createElement)( + "div", + { className: "bt-bs" }, +- (0, import_react18.createElement)( ++ (0, import_react29.createElement)( + "div", + { className: "bt-bs-hero" }, +- (0, import_react18.createElement)( +- "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" }, +- (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") }, +- (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.") }, -+ (0, import_react28.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) ++ (0, import_react29.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) }) + ), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + Row, + { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, -+ (0, import_react28.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_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_react28.createElement)( -+ Card, + ), +- (0, import_react18.createElement)( ++ (0, import_react29.createElement)( + Card, +- { title: t("Model") }, +- (0, import_react18.createElement)( + { title: t("Who answers") }, -+ (0, import_react28.createElement)( -+ "div", ++ (0, import_react29.createElement)( + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), +- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) + { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react28.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react28.createElement)("button", { ++ (0, import_react29.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react29.createElement)("button", { + key: id, + type: "button", + "aria-pressed": mode === id, @@ -11504,108 +15487,119 @@ diff --git a/client.js b/client.js + if (mode !== id) void update({ mode: id }); + } + }, t(label)))), -+ (0, import_react28.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -+ ) -+ ), -+ (0, import_react28.createElement)( -+ Card, -+ { title: t("Working folder") }, -+ (0, import_react28.createElement)( -+ "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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -+ ) -+ ), -+ face ? (0, import_react28.createElement)( -+ Card, ++ (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") }, +- (0, import_react18.createElement)( ++ (0, import_react29.createElement)( + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), ++ (0, import_react29.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) + ) + ), +- (0, import_react18.createElement)( ++ face ? (0, import_react29.createElement)( + 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 }) }) +- ) + { title: t("Members · {count}", { count: people2.length }) }, -+ people2.map((person) => (0, import_react28.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle })) ++ 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_react28.createElement)( ++ face ? null : (0, import_react29.createElement)( + Card, + { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, -+ humans.map((person) => (0, import_react28.createElement)( ++ humans.map((person) => (0, import_react29.createElement)( + "div", + { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "span", + { className: "bt-gs-who" }, -+ (0, import_react28.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), -+ (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Human")), -+ person.role === "admin" ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Admin")) : null ++ (0, import_react29.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id), ++ (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Human")), ++ person.role === "admin" ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null + ), -+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null ++ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null + )), + members.map((member) => { + const leads = room2.admin === member.id; + const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share"; + const taskLead = room2.taskLead === member.id; -+ return (0, import_react28.createElement)( ++ return (0, import_react29.createElement)( + "div", + { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "button", + { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -+ (0, import_react28.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -+ (0, import_react28.createElement)("span", { className: "bt-row-title" }, member.name), -+ leads ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, -+ taskLead ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, -+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Chief")) : null ++ (0, import_react29.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), ++ (0, import_react29.createElement)("span", { className: "bt-row-title" }, member.name), ++ leads ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null, ++ taskLead ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null, ++ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Chief")) : null + ), -+ leads ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -+ boardHere ? taskLead ? (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react28.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, -+ members.length > 2 ? (0, import_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null ++ leads ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), ++ boardHere ? taskLead ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null, ++ members.length > 2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null + ); + }), -+ remotes.map((person) => (0, import_react28.createElement)( ++ remotes.map((person) => (0, import_react29.createElement)( + "div", + { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "span", + { className: "bt-gs-who" }, -+ (0, import_react28.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react28.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), ++ (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), ++ (0, import_react29.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)), + // #904: a host's roster entry is not a friend's Bot, and one this home cannot confirm is + // labelled as such; a Bot with no Bot Social identity can never be written to at all. -+ room2.origin === "face-to-face" ? null : (0, import_react28.createElement)("span", { className: "bt-tag" }, person.unconnected ? t("Not connected") : room2.origin === "inbound-share" && person.verified === false ? t("Unconfirmed") : t("Friend's Bot")) ++ 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")) + ), + // 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_react28.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×") ++ room2.origin === "face-to-face" || 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 }] }) }, "×") + )), + room2.origin === "face-to-face" ? null : adding ? [ -+ ...outsidePeople.map((person) => (0, import_react28.createElement)( ++ ...outsidePeople.map((person) => (0, import_react29.createElement)( + "button", + { key: person.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { + setAdding(false); + void update({ addParticipants: [{ kind: "human", id: person.id, displayName: person.displayName }] }); + } }, -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), -+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add person")) ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)), ++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add person")) + )), -+ ...outside.map((bot) => (0, import_react28.createElement)( ++ ...outside.map((bot) => (0, import_react29.createElement)( + "button", + { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { + setAdding(false); + void update({ add: [bot.id] }); + } }, -+ (0, import_react28.createElement)(BotMark, { bot, size: 22 }), -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, bot.name)), -+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add")) ++ (0, import_react29.createElement)(BotMark, { bot, size: 22 }), ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, bot.name)), ++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add")) + )), -+ ...outsideRemotes.map((bot) => (0, import_react28.createElement)( ++ ...outsideRemotes.map((bot) => (0, import_react29.createElement)( + "button", + { key: bot.handle, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { + setAdding(false); + void update({ addParticipants: [{ kind: "bot", remote: true, handle: bot.handle }] }); + } }, -+ (0, import_react28.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), -+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) ++ (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }), ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))), ++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot")) + )), -+ (0, import_react28.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -+ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react28.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react28.createElement)(PlusIcon), t("Add member")) : null, -+ (0, import_react28.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react28.createElement)( ++ (0, import_react29.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) ++ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react29.createElement)(PlusIcon), t("Add member")) : null, ++ (0, import_react29.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react29.createElement)( + "span", + { className: "bt-set-hint" }, + // The host checks for two members on every change, even a rename. @@ -11613,37 +15607,43 @@ diff --git a/client.js b/client.js + )), + // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after + // the note on who leads, so that note stays with the members it is about (#892). -+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react28.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) -+ ), -+ (0, import_react28.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), -+ (0, import_react28.createElement)( -+ Card, ++ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react29.createElement)(FlyBotRoomMembers, { room: room2, roster, actions }) + ), +- (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)( ++ (0, import_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), ++ (0, import_react29.createElement)( + Card, +- { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + { title: t("Sidebar") }, -+ (0, import_react28.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_react28.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_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_react28.createElement)( ++ (0, import_react29.createElement)( + Card, + { title: t("More") }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "button", + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "span", + { className: "bt-set-copy" }, -+ (0, import_react28.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -+ (0, import_react28.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_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), ++ (0, import_react29.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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) ++ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) + ) + ) + ); +} +function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -+ const [admin, setAdmin] = (0, import_react28.useState)(null); -+ const [members, setMembers] = (0, import_react28.useState)([]); -+ const [name, setName] = (0, import_react28.useState)(""); -+ const [busy, setBusy] = (0, import_react28.useState)(false); ++ const [admin, setAdmin] = (0, import_react29.useState)(null); ++ const [members, setMembers] = (0, import_react29.useState)([]); ++ const [name, setName] = (0, import_react29.useState)(""); ++ const [busy, setBusy] = (0, import_react29.useState)(false); + const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const pickAdmin = (id) => { + setAdmin(id); @@ -11659,48 +15659,48 @@ diff --git a/client.js b/client.js + if (done && room2) onCreated(room2); + else setBusy(false); + }; -+ const chip = (bot, on, onClick, role) => (0, import_react28.createElement)("button", { ++ const chip = (bot, on, onClick, role) => (0, import_react29.createElement)("button", { + key: bot.id, + type: "button", + role, + className: "bt-ng-chip", + "aria-checked": on, + onClick -+ }, (0, import_react28.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react28.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react28.createElement)(AdminStar) : null); -+ return (0, import_react28.createElement)( ++ }, (0, import_react29.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react29.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react29.createElement)(AdminStar) : null); ++ return (0, import_react29.createElement)( + "div", + { className: "bt-set-card bt-ng" }, -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "div", + { className: "bt-set-row bt-bs-row bt-bs-stack" }, -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react28.createElement)(AdminStar), t("Choose the group admin"))), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react29.createElement)(AdminStar), t("Choose the group admin"))), ++ (0, import_react29.createElement)( + "div", + { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, + roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + ) + ), -+ admin === null ? null : (0, import_react28.createElement)( ++ admin === null ? null : (0, import_react29.createElement)( + "div", + { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), ++ (0, import_react29.createElement)( + "div", + { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, + roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) + ) + ), -+ admin === null || members.length === 0 ? null : (0, import_react28.createElement)( ++ admin === null || members.length === 0 ? null : (0, import_react29.createElement)( + "label", + { className: "bt-set-row bt-bs-row bt-ng-step" }, -+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -+ (0, import_react28.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) ++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Name"))), ++ (0, import_react29.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) + ), -+ (0, import_react28.createElement)( ++ (0, import_react29.createElement)( + "div", + { className: "bt-set-row bt-task-foot" }, -+ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -+ (0, import_react28.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ++ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), ++ (0, import_react29.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) + ) + ); +} @@ -11736,13 +15736,13 @@ diff --git a/client.js b/client.js +`; + +// src/client/palette.js -+var import_react29 = require("react"); ++var import_react30 = require("react"); +var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); +var PALETTE_EXIT_MS = 160; +function CommandPalette({ start, token, roster, actions, useSessions }) { -+ const [query, setQuery] = (0, import_react29.useState)(start ?? ""); -+ const [cursor, setCursor] = (0, import_react29.useState)(0); -+ const [leaving, setLeaving] = (0, import_react29.useState)(false); ++ const [query, setQuery] = (0, import_react30.useState)(start ?? ""); ++ const [cursor, setCursor] = (0, import_react30.useState)(0); ++ const [leaving, setLeaving] = (0, import_react30.useState)(false); + const wanted = query.trim().toLowerCase(); + const close = () => { + if (leaving) return; @@ -11778,7 +15778,7 @@ diff --git a/client.js b/client.js + const people2 = [ + ...roster.bots.filter((bot) => isListedRemoteChat(bot) && matchesName(bot.name, bot.role, pickerBotLabel(bot))).map((bot) => ({ + key: bot.id, -+ avatar: (0, import_react29.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), ++ avatar: (0, import_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), + title: pickerBotLabel(bot), + subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), + badge: bot.hidden ? t("Hidden") : void 0, @@ -11789,7 +15789,7 @@ diff --git a/client.js b/client.js + })), + ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ + key: room2.id, -+ avatar: (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), ++ avatar: (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), + title: room2.name, + subtitle: t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) }), + badge: room2.hidden ? t("Hidden") : void 0, @@ -11802,7 +15802,7 @@ diff --git a/client.js b/client.js + if (people2.length) sections.push([t("Bots"), people2]); + const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ + key: agent.id, -+ avatar: (0, import_react29.createElement)("span", { className: "bt-palette-agent" }, (0, import_react29.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), ++ avatar: (0, import_react30.createElement)("span", { className: "bt-palette-agent" }, (0, import_react30.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), + title: "DSH Agent", + subtitle: t("Plain dsh session"), + run: () => actions.openAgent(agent.id) @@ -11815,16 +15815,16 @@ diff --git a/client.js b/client.js + key: `${id}:${turn}`, + title: bot?.name ?? room2?.name ?? t("Bot"), + subtitle: snippet, -+ avatar: bot ? (0, import_react29.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), ++ avatar: bot ? (0, import_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), + run: () => actions.openSession(id) + }; + }); + if (messages.length) sections.push([t("Messages"), messages]); + const commands = [ -+ { key: "cmd:new", icon: (0, import_react29.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -+ { key: "cmd:bot", icon: (0, import_react29.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -+ { key: "cmd:group", icon: (0, import_react29.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -+ { key: "cmd:settings", icon: (0, import_react29.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") } ++ { key: "cmd:new", icon: (0, import_react30.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, ++ { key: "cmd:bot", icon: (0, import_react30.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, ++ { key: "cmd:group", icon: (0, import_react30.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, ++ { key: "cmd:settings", icon: (0, import_react30.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") } + ].filter((command) => matchesName(command.title)); + if (commands.length) sections.push([t("Commands"), commands]); + const rows = sections.flatMap(([, items]) => items); @@ -11834,7 +15834,7 @@ diff --git a/client.js b/client.js +${shown}`, { axis: "y", ...QUICK_EDGES }); + const listRef = glide.box; + useFlip(listRef, shown, "[data-flip]"); -+ (0, import_react29.useEffect)(() => { ++ (0, import_react30.useEffect)(() => { + listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); + }, [active, query]); + const choose = (row) => { @@ -11843,18 +15843,43 @@ diff --git a/client.js b/client.js + close(); + }; + let index = -1; -+ return (0, import_react29.createElement)( ++ return (0, import_react30.createElement)( + "div", + { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -+ (0, import_react29.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -+ (0, import_react29.createElement)( ++ (0, import_react30.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), ++ (0, import_react30.createElement)( + "div", + { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -+ (0, import_react29.createElement)( -+ "div", ++ (0, import_react30.createElement)( + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) +- ), +- (0, import_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)) }) +- ), +- // The Main Bot tile is always first and always shown, like in the context menu. +- isTile ? null : (0, import_react18.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), +- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + { className: "bt-palette-head" }, -+ (0, import_react29.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react29.createElement)(SearchIcon)), -+ (0, import_react29.createElement)("input", { ++ (0, import_react30.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)), ++ (0, import_react30.createElement)("input", { + autoFocus: true, + value: query, + placeholder: t("Search Bots, chats and messages"), @@ -11880,29 +15905,39 @@ diff --git a/client.js b/client.js + } + } + }), -+ query ? (0, import_react29.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { ++ query ? (0, import_react30.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { + setQuery(""); + setCursor(0); -+ } }, (0, import_react29.createElement)(CloseIcon)) : null -+ ), -+ (0, import_react29.createElement)( ++ } }, (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") }, -+ (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ rows.length === 0 ? (0, import_react29.createElement)( ++ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ rows.length === 0 ? (0, import_react30.createElement)( + "div", + { className: "bt-palette-empty" }, -+ (0, import_react29.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react29.createElement)(SearchIcon)), -+ (0, import_react29.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -+ (0, import_react29.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -+ ) : sections.map(([title, items]) => (0, import_react29.createElement)( -+ import_react29.Fragment, ++ (0, import_react30.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)), ++ (0, import_react30.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), ++ (0, import_react30.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) ++ ) : sections.map(([title, items]) => (0, import_react30.createElement)( ++ import_react30.Fragment, + { key: title }, -+ (0, import_react29.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), ++ (0, import_react30.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), + items.map((row) => { + index += 1; + const at = index; -+ return (0, import_react29.createElement)( ++ return (0, import_react30.createElement)( + "button", + { + key: row.key, @@ -11916,25 +15951,57 @@ diff --git a/client.js b/client.js + }, + onClick: () => choose(row) + }, -+ (0, import_react29.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react29.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -+ (0, import_react29.createElement)( ++ (0, import_react30.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react30.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), ++ (0, import_react30.createElement)( + "span", + { className: "bt-palette-text" }, -+ (0, import_react29.createElement)("span", { className: "bt-palette-title" }, row.title), -+ row.subtitle ? (0, import_react29.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null ++ (0, import_react30.createElement)("span", { className: "bt-palette-title" }, row.title), ++ row.subtitle ? (0, import_react30.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null + ), -+ row.badge ? (0, import_react29.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null ++ row.badge ? (0, import_react30.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null + ); + }) + )) -+ ) -+ ) -+ ); -+} -+ + ) + ) + ); + } +-var BOT_SETTINGS_CSS = ` +-.bt-bs{display:flex;flex-direction:column;gap:18px} +-.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} +-.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} +-.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-bs .bt-avedit{left:0;transform:none} +-.bt-bs-row{min-height:48px} +-label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} +-.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +-.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +-.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} +-.bt-bs-input:hover{background:var(--bt-hover)} +-.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +-.bt-bs-input::placeholder{color:var(--bt-ink-3)} +-.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} +-.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} +-.bt-bs-area:hover{background:var(--bt-active)} +-.bt-bs-danger .bt-bs-label{color:#c21d2e} +-.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} +-.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} +-.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} +-.bt-switch[aria-checked=true]{background:var(--bt-accent)} +-.bt-switch:disabled{opacity:.45;cursor:default} +-.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} +-.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} +-.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} +-.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} +-.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} +-.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} +-@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +-`; + // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { - const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); @@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { } function startModel(roster) { @@ -11966,7 +16033,7 @@ diff --git a/client.js b/client.js // src/client/create-bot.js -var import_react19 = require("react"); -+var import_react30 = require("react"); ++var import_react31 = require("react"); var pick = (list) => list[Math.floor(Math.random() * list.length)]; var randomLook = (avoid = {}) => { const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); @@ -11984,18 +16051,18 @@ diff --git a/client.js b/client.js - const fileRef = (0, import_react19.useRef)(null); - const frame = (0, import_react19.useRef)(null); +function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) { -+ const [look, setLook] = (0, import_react30.useState)(() => randomLook()); -+ const [image, setImage] = (0, import_react30.useState)(null); -+ const [name, setName] = (0, import_react30.useState)(start?.name ?? ""); -+ const [model, setModel] = (0, import_react30.useState)(() => startModel(roster)); -+ const [effort, setEffort] = (0, import_react30.useState)(void 0); -+ const [busy, setBusy] = (0, import_react30.useState)(false); -+ const [error, setError] = (0, import_react30.useState)(""); -+ const fileRef = (0, import_react30.useRef)(null); -+ const frame = (0, import_react30.useRef)(null); ++ const [look, setLook] = (0, import_react31.useState)(() => randomLook()); ++ const [image, setImage] = (0, import_react31.useState)(null); ++ const [name, setName] = (0, import_react31.useState)(start?.name ?? ""); ++ const [model, setModel] = (0, import_react31.useState)(() => startModel(roster)); ++ const [effort, setEffort] = (0, import_react31.useState)(void 0); ++ const [busy, setBusy] = (0, import_react31.useState)(false); ++ const [error, setError] = (0, import_react31.useState)(""); ++ const fileRef = (0, import_react31.useRef)(null); ++ const frame = (0, import_react31.useRef)(null); useEscape(onBack); - (0, import_react19.useEffect)(() => { -+ (0, import_react30.useEffect)(() => { ++ (0, import_react31.useEffect)(() => { if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); @@ -12015,32 +16082,32 @@ diff --git a/client.js b/client.js }; const previewLook = image ? { ...look, image } : look; - const card = (0, import_react19.createElement)( -+ const card = (0, import_react30.createElement)( ++ const card = (0, import_react31.createElement)( "div", { className: "bt-create-card" }, - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "div", { className: "bt-create-preview" }, // The key remounts the mark, so every new pick plays the pop. - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "span", { key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" }, - (0, import_react19.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) -+ (0, import_react30.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) ++ (0, import_react31.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true }) ), - (0, import_react19.createElement)("span", { className: "bt-create-name" }, finalName) -+ (0, import_react30.createElement)("span", { className: "bt-create-name" }, finalName) ++ (0, import_react31.createElement)("span", { className: "bt-create-name" }, finalName) ), - (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")), - (0, import_react19.createElement)( -+ (0, import_react30.createElement)("div", { className: "bt-create-step" }, t("Look")), -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)("div", { className: "bt-create-step" }, t("Look")), ++ (0, import_react31.createElement)( "div", { className: "bt-create-looks" }, - (0, import_react19.createElement)(CharacterGrid, { -+ (0, import_react30.createElement)(CharacterGrid, { ++ (0, import_react31.createElement)(CharacterGrid, { look, hasPhoto: Boolean(image), onShape: (shape) => { @@ -12049,20 +16116,20 @@ diff --git a/client.js b/client.js } }), - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "div", { className: "bt-actions bt-create-lookbar" }, - (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => { setImage(null); setLook((current) => randomLook(current)); } }, t("Shuffle")), - (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), - image ? (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, - (0, import_react19.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), -+ image ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, -+ (0, import_react30.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")), ++ image ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null, ++ (0, import_react31.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { const file = event.target.files?.[0]; event.target.value = ""; upload(file); @@ -12070,13 +16137,13 @@ diff --git a/client.js b/client.js ) ), - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "label", { className: "bt-field" }, - (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")), - (0, import_react19.createElement)("input", { -+ (0, import_react30.createElement)("span", { className: "bt-field-label" }, t("Name")), -+ (0, import_react30.createElement)("input", { ++ (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Name")), ++ (0, import_react31.createElement)("input", { className: "bt-input", autoFocus: !inline, value: name, @@ -12085,61 +16152,61 @@ diff --git a/client.js b/client.js } }), - problem ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null -+ problem ? (0, import_react30.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react30.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null ++ problem ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null ), - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "div", { className: "bt-field" }, - (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Model")), - (0, import_react19.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { -+ (0, import_react30.createElement)("span", { className: "bt-field-label" }, t("Model")), -+ (0, import_react30.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { ++ (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Model")), ++ (0, import_react31.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => { setModel(ref); setEffort(void 0); }, onPickEffort: setEffort }), - (0, import_react19.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) -+ (0, import_react30.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) ++ (0, import_react31.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later.")) ), - error ? (0, import_react19.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, - (0, import_react19.createElement)( -+ error ? (0, import_react30.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ (0, import_react30.createElement)( ++ error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, ++ (0, import_react31.createElement)( "div", { className: "bt-create-foot" }, - (0, import_react19.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")), - (0, import_react19.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) + // The dialog's header already has Back and Close (#230); only the inline form needs its own. -+ inline ? (0, import_react30.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) ++ inline ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null, ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create")) ) ); - if (inline) return (0, import_react19.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); - return (0, import_react19.createElement)( -+ if (inline) return (0, import_react30.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); -+ return (0, import_react30.createElement)( ++ if (inline) return (0, import_react31.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card); ++ return (0, import_react31.createElement)( "div", { className: "bt-create", role: "dialog", "aria-label": t("Create Bot") }, - (0, import_react19.createElement)( -+ (0, import_react30.createElement)( ++ (0, import_react31.createElement)( "div", { className: "bt-create-head" }, - canGoBack ? (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react19.createElement)(ChevronLeftIcon)) : (0, import_react19.createElement)("span", { className: "bt-create-gap" }), - (0, import_react19.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), - (0, import_react19.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react19.createElement)(CloseIcon)) -+ canGoBack ? (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react30.createElement)(ChevronLeftIcon)) : (0, import_react30.createElement)("span", { className: "bt-create-gap" }), -+ (0, import_react30.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), -+ (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react30.createElement)(CloseIcon)) ++ canGoBack ? (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.createElement)("span", { className: "bt-create-gap" }), ++ (0, import_react31.createElement)("span", { className: "bt-create-title" }, t("Create Bot")), ++ (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react31.createElement)(CloseIcon)) ), - (0, import_react19.createElement)("div", { className: "bt-create-body" }, card) -+ (0, import_react30.createElement)("div", { className: "bt-create-body" }, card) ++ (0, import_react31.createElement)("div", { className: "bt-create-body" }, card) ); } // src/client/new-chat.js -var import_react20 = require("react"); -var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives"); -+var import_react31 = require("react"); ++var import_react32 = require("react"); function NewChat({ mode, roster, actions }) { - const [query, setQuery] = (0, import_react20.useState)(""); - const [picked, setPicked] = (0, import_react20.useState)([]); @@ -12151,20 +16218,20 @@ diff --git a/client.js b/client.js - const [cursor, setCursor] = (0, import_react20.useState)(0); - const [draft, setDraft] = (0, import_react20.useState)(mode === "create" ? { name: "", fromList: false } : null); - const inputRef = (0, import_react20.useRef)(null); -+ const [query, setQuery] = (0, import_react31.useState)(""); -+ const [picked, setPicked] = (0, import_react31.useState)([]); -+ const [group, setGroup] = (0, import_react31.useState)(mode === "group"); -+ const [compose, setCompose] = (0, import_react31.useState)(mode !== "new"); -+ const [admin, setAdmin] = (0, import_react31.useState)(null); -+ const [message, setMessage] = (0, import_react31.useState)(""); -+ const [busy, setBusy] = (0, import_react31.useState)(false); -+ const [error, setError] = (0, import_react31.useState)(""); -+ const [fix, setFix] = (0, import_react31.useState)(null); -+ const [fixNote, setFixNote] = (0, import_react31.useState)(""); -+ const [inviteLink, setInviteLink] = (0, import_react31.useState)(""); -+ const [cursor, setCursor] = (0, import_react31.useState)(0); -+ const [draft, setDraft] = (0, import_react31.useState)(mode === "create" ? { name: "", fromList: false } : null); -+ const inputRef = (0, import_react31.useRef)(null); ++ const [query, setQuery] = (0, import_react32.useState)(""); ++ const [picked, setPicked] = (0, import_react32.useState)([]); ++ const [group, setGroup] = (0, import_react32.useState)(mode === "group"); ++ const [compose, setCompose] = (0, import_react32.useState)(mode !== "new"); ++ const [admin, setAdmin] = (0, import_react32.useState)(null); ++ const [message, setMessage] = (0, import_react32.useState)(""); ++ const [busy, setBusy] = (0, import_react32.useState)(false); ++ const [error, setError] = (0, import_react32.useState)(""); ++ const [fix, setFix] = (0, import_react32.useState)(null); ++ const [fixNote, setFixNote] = (0, import_react32.useState)(""); ++ const [inviteLink, setInviteLink] = (0, import_react32.useState)(""); ++ const [cursor, setCursor] = (0, import_react32.useState)(0); ++ const [draft, setDraft] = (0, import_react32.useState)(mode === "create" ? { name: "", fromList: false } : null); ++ const inputRef = (0, import_react32.useRef)(null); const close = () => actions.closeOverlay(); useEscape(close); const paneLeft = usePaneLeft(); @@ -12266,14 +16333,14 @@ diff --git a/client.js b/client.js - options.push({ key: "new", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot"), run: createBot }); - options.push({ key: "group", icon: (0, import_react20.createElement)(PeopleIcon), label: t("Create group chat"), run: () => { + if (menuOnly) { -+ options.push({ key: "chat", icon: (0, import_react31.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { ++ options.push({ key: "chat", icon: (0, import_react32.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => { + setCompose(true); + setCursor(0); + setTimeout(() => inputRef.current?.focus()); + } }); + } -+ options.push({ key: "new", icon: (0, import_react31.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); -+ options.push({ key: "group", icon: (0, import_react31.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { ++ options.push({ key: "new", icon: (0, import_react32.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot }); ++ options.push({ key: "group", icon: (0, import_react32.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => { setGroup(true); + setCompose(true); setCursor(0); @@ -12282,7 +16349,7 @@ diff --git a/client.js b/client.js + for (const action of newChatActionRows({ mode, group, picked, query, roster })) { + options.push({ + key: action.key, -+ icon: action.icon ? (0, import_react31.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react31.createElement)(PeopleIcon) }) : (0, import_react31.createElement)(PeopleIcon), ++ icon: action.icon ? (0, import_react32.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react32.createElement)(PeopleIcon) }) : (0, import_react32.createElement)(PeopleIcon), + label: t(action.label), + hint: action.hint ? t(action.hint) : void 0, + run: () => { @@ -12295,7 +16362,7 @@ diff --git a/client.js b/client.js + } if ((roster.agents ?? []).length === 0) { - options.push({ key: "agent", icon: (0, import_react20.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add DSH Agent"), run: () => run(async () => { -+ options.push({ key: "agent", icon: (0, import_react31.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { ++ options.push({ key: "agent", icon: (0, import_react32.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => { await actions.addAgent(); close(); }) }); @@ -12309,22 +16376,22 @@ diff --git a/client.js b/client.js + } } - if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react20.createElement)(PlusIcon), label: t("Create new Bot “{name}”", { name: query.trim() }), run: createBot }); -+ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react31.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); ++ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react32.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot }); const active = Math.min(cursor, Math.max(options.length - 1, 0)); const listed = options.map((option) => option.key).join("\n"); const glide = useGlide(`${active} ${listed}`, { axis: "y", ...QUICK_EDGES }); useFlip(glide.box, listed, "[data-flip]"); - const drop = (0, import_react20.useRef)(null); -+ const drop = (0, import_react31.useRef)(null); ++ const drop = (0, import_react32.useRef)(null); useMorph(drop, listed); if (draft) { - return (0, import_react20.createElement)( -+ return (0, import_react31.createElement)( ++ return (0, import_react32.createElement)( "div", { className: "bt-pane", style: { left: paneLeft } }, - (0, import_react20.createElement)(CreateBot, { -+ (0, import_react31.createElement)(CreateBot, { ++ (0, import_react32.createElement)(CreateBot, { roster, actions, start: draft, @@ -12337,7 +16404,7 @@ diff --git a/client.js b/client.js else if (admin !== null) setAdmin(null); }; - return (0, import_react20.createElement)( -+ return (0, import_react31.createElement)( ++ return (0, import_react32.createElement)( "div", - { className: "bt-pane", style: { left: paneLeft } }, - (0, import_react20.createElement)( @@ -12349,43 +16416,43 @@ diff --git a/client.js b/client.js + if (event.target === event.currentTarget) close(); + } : void 0 + }, -+ (0, import_react31.createElement)( ++ (0, import_react32.createElement)( "div", - { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : t("New chat") }, - (0, import_react20.createElement)( + { className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : menuOnly ? t("New…") : t("New chat"), "data-menu": menuOnly || void 0 }, + // The popover needs no title row: Escape or a press outside closes it. -+ menuOnly ? null : (0, import_react31.createElement)( ++ menuOnly ? null : (0, import_react32.createElement)( "div", { className: "bt-to" }, - (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")), - adminBot ? (0, import_react20.createElement)( -+ (0, import_react31.createElement)("span", { className: "bt-to-label" }, t("To:")), -+ adminBot ? (0, import_react31.createElement)( ++ (0, import_react32.createElement)("span", { className: "bt-to-label" }, t("To:")), ++ adminBot ? (0, import_react32.createElement)( "span", { key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") }, - (0, import_react20.createElement)(BotMark, { bot: adminBot, size: 16 }), -+ (0, import_react31.createElement)(BotMark, { bot: adminBot, size: 16 }), ++ (0, import_react32.createElement)(BotMark, { bot: adminBot, size: 16 }), adminBot.name, - (0, import_react20.createElement)(AdminStar), - (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") -+ (0, import_react31.createElement)(AdminStar), -+ (0, import_react31.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") ++ (0, import_react32.createElement)(AdminStar), ++ (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×") ) : null, - choosingAdmin ? (0, import_react20.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react20.createElement)(AdminStar)) : null, - pickedBots.map((bot) => (0, import_react20.createElement)( -+ choosingAdmin ? (0, import_react31.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react31.createElement)(AdminStar)) : null, -+ pickedBots.map((bot) => (0, import_react31.createElement)( ++ choosingAdmin ? (0, import_react32.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react32.createElement)(AdminStar)) : null, ++ pickedBots.map((bot) => (0, import_react32.createElement)( "span", { key: bot.id, className: "bt-token" }, - (0, import_react20.createElement)(BotMark, { bot, size: 16 }), -+ (0, import_react31.createElement)(BotMark, { bot, size: 16 }), ++ (0, import_react32.createElement)(BotMark, { bot, size: 16 }), bot.name, - (0, import_react20.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") -+ (0, import_react31.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") ++ (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×") )), - (0, import_react20.createElement)("input", { -+ (0, import_react31.createElement)("input", { ++ (0, import_react32.createElement)("input", { ref: inputRef, autoFocus: true, value: query, @@ -12394,7 +16461,7 @@ diff --git a/client.js b/client.js } }), - group && admin !== null ? (0, import_react20.createElement)("button", { -+ group && admin !== null ? (0, import_react31.createElement)("button", { ++ group && admin !== null ? (0, import_react32.createElement)("button", { type: "button", className: "bt-send bt-to-create", disabled: busy || picked.length === 0, @@ -12402,16 +16469,16 @@ diff --git a/client.js b/client.js onClick: createGroup }, t("Create group chat")) : null, - (0, import_react20.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") -+ (0, import_react31.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") ++ (0, import_react32.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×") ), - (0, import_react20.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react20.createElement)( -+ (0, import_react31.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react31.createElement)( ++ (0, import_react32.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react32.createElement)( "div", { ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" }, - (0, import_react20.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - options.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react20.createElement)( -+ (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.length === 0 ? (0, import_react31.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react31.createElement)( ++ (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ options.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react32.createElement)( "button", { key: option.key, @@ -12422,42 +16489,42 @@ diff --git a/client.js b/client.js - option.bot ? (0, import_react20.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react20.createElement)("span", { className: "bt-option-icon" }, option.icon), - (0, import_react20.createElement)("span", { className: "bt-option-label" }, option.label), - option.hint && index === active ? (0, import_react20.createElement)("span", { className: "bt-option-hint" }, option.hint) : null -+ option.bot ? (0, import_react31.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react31.createElement)("span", { className: "bt-option-icon" }, option.icon), ++ option.bot ? (0, import_react32.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react32.createElement)("span", { className: "bt-option-icon" }, option.icon), + // An action row's description sits under its title and is always there (#283): a long line + // beside the title squeezed the title to nothing on hover, and a line that appears on hover moves the rows. + // A Bot row's short hint stays beside the name and shows on the active row only. -+ option.bot || !option.hint ? (0, import_react31.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react31.createElement)("span", { className: "bt-option-copy" }, (0, import_react31.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react31.createElement)("span", { className: "bt-option-sub" }, option.hint)), -+ option.bot && option.hint && index === active ? (0, import_react31.createElement)("span", { className: "bt-option-hint" }, option.hint) : null ++ option.bot || !option.hint ? (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react32.createElement)("span", { className: "bt-option-copy" }, (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react32.createElement)("span", { className: "bt-option-sub" }, option.hint)), ++ option.bot && option.hint && index === active ? (0, import_react32.createElement)("span", { className: "bt-option-hint" }, option.hint) : null )) )), - (0, import_react20.createElement)("div", { className: "bt-nc-spacer" }), - error ? (0, import_react20.createElement)("div", { className: "bt-error" }, error) : null, - (0, import_react20.createElement)( -+ menuOnly ? null : (0, import_react31.createElement)("div", { className: "bt-nc-spacer" }), -+ error && fix ? (0, import_react31.createElement)( ++ menuOnly ? null : (0, import_react32.createElement)("div", { className: "bt-nc-spacer" }), ++ error && fix ? (0, import_react32.createElement)( + "div", + { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" }, -+ (0, import_react31.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), -+ (0, import_react31.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ -+ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react31.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { ++ (0, import_react32.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })), ++ (0, import_react32.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [ ++ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react32.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => { + void copyInvite(bot); + } }, t("Copy invite link for {name}", { name: bot.name })) : null, -+ (0, import_react31.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) ++ (0, import_react32.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name })) + ])), -+ fixNote ? (0, import_react31.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, -+ inviteLink ? (0, import_react31.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null -+ ) : error ? (0, import_react31.createElement)("div", { className: "bt-error" }, error) : null, -+ menuOnly ? null : (0, import_react31.createElement)( ++ fixNote ? (0, import_react32.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null, ++ inviteLink ? (0, import_react32.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null ++ ) : error ? (0, import_react32.createElement)("div", { className: "bt-error" }, error) : null, ++ menuOnly ? null : (0, import_react32.createElement)( "div", { className: "bt-nc-compose" }, - (0, import_react20.createElement)( -+ (0, import_react31.createElement)( ++ (0, import_react32.createElement)( "div", { className: "bt-nc-card" }, - (0, import_react20.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react20.createElement)(PlusIcon)), - (0, import_react20.createElement)("textarea", { -+ (0, import_react31.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react31.createElement)(PlusIcon)), -+ (0, import_react31.createElement)("textarea", { ++ (0, import_react32.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react32.createElement)(PlusIcon)), ++ (0, import_react32.createElement)("textarea", { rows: 1, value: message, placeholder, @@ -12466,7 +16533,7 @@ diff --git a/client.js b/client.js } }), - (0, import_react20.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react20.createElement)(ArrowUpIcon)) -+ (0, import_react31.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react31.createElement)(ArrowUpIcon)) ++ (0, import_react32.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react32.createElement)(ArrowUpIcon)) ) ) ) @@ -12475,13 +16542,13 @@ diff --git a/client.js b/client.js // src/client/exchange.js -var import_react21 = require("react"); -+var import_react32 = require("react"); ++var import_react33 = require("react"); var SHEET_EXIT_MS = 220; function ExchangeDialog({ pair, roster, actions }) { - const [entries2, setEntries] = (0, import_react21.useState)(null); - const [leaving, setLeaving] = (0, import_react21.useState)(false); -+ const [entries2, setEntries] = (0, import_react32.useState)(null); -+ const [leaving, setLeaving] = (0, import_react32.useState)(false); ++ const [entries2, setEntries] = (0, import_react33.useState)(null); ++ const [leaving, setLeaving] = (0, import_react33.useState)(false); const close = () => { if (leaving) return; setLeaving(true); @@ -12489,7 +16556,7 @@ diff --git a/client.js b/client.js }; useEscape(close); - (0, import_react21.useEffect)(() => { -+ (0, import_react32.useEffect)(() => { ++ (0, import_react33.useEffect)(() => { let stop = false; let timer; let seen = null; @@ -12498,63 +16565,63 @@ diff --git a/client.js b/client.js let lastDay = 0; const paneLeft = usePaneLeft(); - return (0, import_react21.createElement)( -+ return (0, import_react32.createElement)( ++ return (0, import_react33.createElement)( "div", { className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 }, - (0, import_react21.createElement)( -+ (0, import_react32.createElement)( ++ (0, import_react33.createElement)( "div", { className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") }, - (0, import_react21.createElement)("div", { className: "bt-head" }, (0, import_react21.createElement)( -+ (0, import_react32.createElement)("div", { className: "bt-head" }, (0, import_react32.createElement)( ++ (0, import_react33.createElement)("div", { className: "bt-head" }, (0, import_react33.createElement)( "span", { className: "bt-pill", style: { cursor: "default" } }, - left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null, -+ left ? (0, import_react32.createElement)(BotMark, { bot: left, size: 22 }) : null, ++ left ? (0, import_react33.createElement)(BotMark, { bot: left, size: 22 }) : null, left?.name ?? "?", - (0, import_react21.createElement)("span", { className: "bt-swap" }, "⇄"), - right ? (0, import_react21.createElement)(BotMark, { bot: right, size: 22 }) : null, -+ (0, import_react32.createElement)("span", { className: "bt-swap" }, "⇄"), -+ right ? (0, import_react32.createElement)(BotMark, { bot: right, size: 22 }) : null, ++ (0, import_react33.createElement)("span", { className: "bt-swap" }, "⇄"), ++ right ? (0, import_react33.createElement)(BotMark, { bot: right, size: 22 }) : null, right?.name ?? "?" )), - (0, import_react21.createElement)( -+ (0, import_react32.createElement)( ++ (0, import_react33.createElement)( "div", { className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") }, - entries2 === null ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { -+ entries2 === null ? (0, import_react32.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { ++ entries2 === null ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => { const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" }; const showDay = entry.time - lastDay > 30 * 60 * 1e3; lastDay = entry.time; - return (0, import_react21.createElement)( - import_react21.Fragment, -+ return (0, import_react32.createElement)( -+ import_react32.Fragment, ++ return (0, import_react33.createElement)( ++ import_react33.Fragment, { key: entry.id }, - showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, - (0, import_react21.createElement)( -+ showDay ? (0, import_react32.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, -+ (0, import_react32.createElement)( ++ showDay ? (0, import_react33.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null, ++ (0, import_react33.createElement)( "div", { className: "bt-group-msg" }, - (0, import_react21.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), - (0, import_react21.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react21.createElement)("span", { className: "bt-lead" }, (0, import_react21.createElement)(BotMark, { bot, size: 22 })) }) -+ (0, import_react32.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), -+ (0, import_react32.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react32.createElement)("span", { className: "bt-lead" }, (0, import_react32.createElement)(BotMark, { bot, size: 22 })) }) ++ (0, import_react33.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name), ++ (0, import_react33.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react33.createElement)("span", { className: "bt-lead" }, (0, import_react33.createElement)(BotMark, { bot, size: 22 })) }) ) ); }) ), - (0, import_react21.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) -+ (0, import_react32.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) ++ (0, import_react33.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat"))) ) ); } // src/client/voice.js -var import_react22 = require("react"); -+var import_react33 = require("react"); ++var import_react34 = require("react"); var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); var VOICE_LABELS = { listening: "Listening", @@ -12564,8 +16631,8 @@ diff --git a/client.js b/client.js function useMicLevel(enabled) { - const [level, setLevel] = (0, import_react22.useState)(0); - (0, import_react22.useEffect)(() => { -+ const [level, setLevel] = (0, import_react33.useState)(0); -+ (0, import_react33.useEffect)(() => { ++ const [level, setLevel] = (0, import_react34.useState)(0); ++ (0, import_react34.useEffect)(() => { if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; let stream = null; let audio = null; @@ -12590,19 +16657,19 @@ diff --git a/client.js b/client.js - const [cycle, setCycle] = (0, import_react22.useState)(0); - const waiting = (0, import_react22.useRef)(null); - const reply = (0, import_react22.useRef)(lastReply); -+ const [speech] = (0, import_react33.useState)(() => actions.speech()); -+ const [phase, setPhase] = (0, import_react33.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); -+ const [catalog, setCatalog] = (0, import_react33.useState)(null); -+ const [heard, setHeard] = (0, import_react33.useState)(""); -+ const [said, setSaid] = (0, import_react33.useState)(""); -+ const [note, setNote] = (0, import_react33.useState)(""); -+ const [serviceLevel, setServiceLevel] = (0, import_react33.useState)(0); -+ const [cycle, setCycle] = (0, import_react33.useState)(0); -+ const waiting = (0, import_react33.useRef)(null); -+ const reply = (0, import_react33.useRef)(lastReply); ++ const [speech] = (0, import_react34.useState)(() => actions.speech()); ++ const [phase, setPhase] = (0, import_react34.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported"); ++ const [catalog, setCatalog] = (0, import_react34.useState)(null); ++ const [heard, setHeard] = (0, import_react34.useState)(""); ++ const [said, setSaid] = (0, import_react34.useState)(""); ++ const [note, setNote] = (0, import_react34.useState)(""); ++ const [serviceLevel, setServiceLevel] = (0, import_react34.useState)(0); ++ const [cycle, setCycle] = (0, import_react34.useState)(0); ++ const waiting = (0, import_react34.useRef)(null); ++ const reply = (0, import_react34.useRef)(lastReply); reply.current = lastReply; - const turn = (0, import_react22.useRef)(0); -+ const turn = (0, import_react33.useRef)(0); ++ const turn = (0, import_react34.useRef)(0); const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; @@ -12611,13 +16678,13 @@ diff --git a/client.js b/client.js submit(text); }); - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); }, [note]); - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { @@ -12626,18 +16693,18 @@ diff --git a/client.js b/client.js }, [speech]); const listen = phase === "listening"; - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (speech || !listen || dictation.listening) return void 0; const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => { } }), 250); return () => clearTimeout(timer); }, [listen, dictation.listening]); - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (phase !== "listening") dictation.stop(); }, [phase]); - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; @@ -12646,81 +16713,74 @@ diff --git a/client.js b/client.js }; }, [speech, catalog, listen, cycle]); - (0, import_react22.useEffect)(() => { -+ (0, import_react33.useEffect)(() => { ++ (0, import_react34.useEffect)(() => { if (phase !== "thinking" || running || waiting.current === null) return; const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; if (count === waiting.current || !text.trim()) return; -@@ -8858,7 +8858,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8858,308 +8858,64 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); - (0, import_react22.useEffect)(() => () => window.speechSynthesis?.cancel(), []); -+ (0, import_react33.useEffect)(() => () => window.speechSynthesis?.cancel(), []); ++ (0, import_react34.useEffect)(() => () => window.speechSynthesis?.cancel(), []); const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,292 +8875,48 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } - const openSetup = phase === "setup" ? actions.voiceSetup() : null; - const caption = phase === "speaking" ? said : heard; - const shownNote = note || dictation.note; + const blocked = phase === "unsupported" || phase === "setup" || phase === "starting"; + const togglePause = () => { + if (phase === "paused") { +- setPhase("listening"); +- return; +- } +- turn.current += 1; +- window.speechSynthesis?.cancel(); +- setPhase("paused"); +- }; +- const openSetup = phase === "setup" ? actions.voiceSetup() : null; +- const caption = phase === "speaking" ? said : heard; +- const shownNote = note || dictation.note; - return (0, import_react22.createElement)( -+ return (0, import_react33.createElement)( - "div", - { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, +- "div", +- { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, - (0, import_react22.createElement)( -+ (0, import_react33.createElement)( - "div", - { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, +- "div", +- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, - (0, import_react22.createElement)( -+ (0, import_react33.createElement)( - "div", - { className: "bt-voice-orb", style: { "--bt-level": level } }, +- "div", +- { className: "bt-voice-orb", style: { "--bt-level": level } }, - (0, import_react22.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), - (0, import_react22.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), - bot ? (0, import_react22.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null -+ (0, import_react33.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), -+ (0, import_react33.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), -+ bot ? (0, import_react33.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react33.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null - ), +- ), - (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name), - (0, import_react22.createElement)( -+ (0, import_react33.createElement)("div", { className: "bt-voice-name" }, name), -+ (0, import_react33.createElement)( - "div", - { className: "bt-voice-status", role: "status" }, +- "div", +- { className: "bt-voice-status", role: "status" }, - phase === "thinking" ? (0, import_react22.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react22.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" -+ phase === "thinking" ? (0, import_react33.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react33.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" - ), +- ), - (0, import_react22.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), - openSetup ? (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { -+ (0, import_react33.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), -+ openSetup ? (0, import_react33.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { - close(); - openSetup(); - } }, t("Set up voice input")) : null, +- close(); +- openSetup(); +- } }, t("Set up voice input")) : null, - shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null -+ shownNote ? (0, import_react33.createElement)("div", { className: "bt-voice-note" }, shownNote) : null - ), +- ), - (0, import_react22.createElement)( -+ (0, import_react33.createElement)( - "div", - { className: "bt-voice-bar" }, +- "div", +- { className: "bt-voice-bar" }, - (0, import_react22.createElement)("button", { -+ (0, import_react33.createElement)("button", { - type: "button", - className: "bt-voice-btn", - "aria-pressed": phase === "paused", - disabled: blocked, - "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), - onClick: togglePause +- type: "button", +- className: "bt-voice-btn", +- "aria-pressed": phase === "paused", +- disabled: blocked, +- "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), +- onClick: togglePause - }, (0, import_react22.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react22.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), - (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react22.createElement)(CloseIcon)) -+ }, (0, import_react33.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react33.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), -+ (0, import_react33.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react33.createElement)(CloseIcon)) - ) - ); - } - +- ) +- ); +-} +- -// src/client/group-settings.js -var import_react23 = require("react"); -var ROOM_MODES = [ @@ -12753,21 +16813,33 @@ diff --git a/client.js b/client.js - const remove = () => { - if (!confirming) { - setConfirming(true); -- return; -- } ++ setPhase("listening"); + return; + } - void Promise.resolve(run(actions.deleteRoom(room2.id))).then((done) => { - if (done) onDeleted?.(); - }); -- }; ++ turn.current += 1; ++ window.speechSynthesis?.cancel(); ++ setPhase("paused"); + }; - return (0, import_react23.createElement)( -- "div", ++ const openSetup = phase === "setup" ? actions.voiceSetup() : null; ++ const caption = phase === "speaking" ? said : heard; ++ const shownNote = note || dictation.note; ++ return (0, import_react34.createElement)( + "div", - { className: "bt-bs" }, - (0, import_react23.createElement)( -- "div", ++ { className: "bt-pane bt-voice-pane", style: { left: paneLeft } }, ++ (0, import_react34.createElement)( + "div", - { className: "bt-bs-hero" }, - (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - (0, import_react23.createElement)( -- "div", ++ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase }, ++ (0, import_react34.createElement)( + "div", - { className: "bt-bs-who" }, - (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name), - (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster)) @@ -12780,7 +16852,11 @@ 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 }) }) -- ), ++ { className: "bt-voice-orb", style: { "--bt-level": level } }, ++ (0, import_react34.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }), ++ (0, import_react34.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }), ++ bot ? (0, import_react34.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null + ), - (0, import_react23.createElement)( - Row, - { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, @@ -12914,7 +16990,9 @@ diff --git a/client.js b/client.js - { 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", ++ (0, import_react34.createElement)("div", { className: "bt-voice-name" }, name), ++ (0, import_react34.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")) - ) @@ -12934,15 +17012,35 @@ diff --git a/client.js b/client.js - { className: "bt-set-row bt-bs-row bt-ng-step" }, - (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), - (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), ++ { className: "bt-voice-status", role: "status" }, ++ phase === "thinking" ? (0, import_react34.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react34.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : "" ++ ), ++ (0, import_react34.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption), ++ openSetup ? (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => { ++ close(); ++ openSetup(); ++ } }, t("Set up voice input")) : null, ++ shownNote ? (0, import_react34.createElement)("div", { className: "bt-voice-note" }, shownNote) : null + ), - (0, import_react23.createElement)( -- "div", ++ (0, import_react34.createElement)( + "div", - { className: "bt-set-row bt-task-foot" }, - (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), - (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) -- ) -- ); --} ++ { className: "bt-voice-bar" }, ++ (0, import_react34.createElement)("button", { ++ type: "button", ++ className: "bt-voice-btn", ++ "aria-pressed": phase === "paused", ++ disabled: blocked, ++ "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"), ++ onClick: togglePause ++ }, (0, import_react34.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react34.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null), ++ (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react34.createElement)(CloseIcon)) + ) + ); + } -var GROUP_SETTINGS_CSS = ` -.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -.bt-ng-step{animation:bt-veil-in .22s ease-out} @@ -12964,10 +17062,9 @@ diff --git a/client.js b/client.js -.bt-bs-stack .bt-seg{align-self:stretch} -@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}} -`; -- + // src/client/usage-data.js var HOUR = 0; - var OWNER = 1; @@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { } return result; @@ -12989,7 +17086,7 @@ diff --git a/client.js b/client.js // src/client/usage-page.js -var import_react24 = require("react"); -+var import_react34 = require("react"); ++var import_react35 = require("react"); var HEAT_WEEKS = 53; var TOP_SERIES = 7; var RING_R = 42; @@ -13005,9 +17102,9 @@ diff --git a/client.js b/client.js - const [shown, setShown] = (0, import_react24.useState)(0); - const from = (0, import_react24.useRef)(0); - (0, import_react24.useEffect)(() => { -+ const [shown, setShown] = (0, import_react34.useState)(0); -+ const from = (0, import_react34.useRef)(0); -+ (0, import_react34.useEffect)(() => { ++ const [shown, setShown] = (0, import_react35.useState)(0); ++ const from = (0, import_react35.useRef)(0); ++ (0, import_react35.useEffect)(() => { if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); @@ -13016,10 +17113,10 @@ diff --git a/client.js b/client.js return shown; } -var glyphBox = (size, icon2) => (0, import_react24.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react24.createElement)(icon2)); -+var glyphBox = (size, icon3) => (0, import_react34.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react34.createElement)(icon3)); ++var glyphBox = (size, icon3) => (0, import_react35.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react35.createElement)(icon3)); function ModelGlyph({ name, paint, size }) { - return (0, import_react24.createElement)("span", { -+ return (0, import_react34.createElement)("span", { ++ return (0, import_react35.createElement)("span", { className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } @@ -13028,7 +17125,7 @@ diff --git a/client.js b/client.js paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], - glyph: (size) => (0, import_react24.createElement)(BotAvatar, { bot, size, badge: false, live: false }) -+ glyph: (size) => (0, import_react34.createElement)(BotAvatar, { bot, size, badge: false, live: false }) ++ glyph: (size) => (0, import_react35.createElement)(BotAvatar, { bot, size, badge: false, live: false }) }; } if (owner.kind === "room") { @@ -13037,7 +17134,7 @@ diff --git a/client.js b/client.js sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", - glyph: (size) => room2 ? (0, import_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) -+ glyph: (size) => room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) ++ glyph: (size) => room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon) }; } if (owner.kind === "agent") { @@ -13046,13 +17143,13 @@ diff --git a/client.js b/client.js const glide = useGlide(`${value} ${names}`, { box: boxRef }); - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "div", { ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label }, - (0, import_react24.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - options.map(([id, text, icon2]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon2 ? (0, import_react24.createElement)(icon2) : null, text)) -+ (0, import_react34.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ options.map(([id, text, icon3]) => (0, import_react34.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react34.createElement)(icon3) : null, text)) ++ (0, import_react35.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ options.map(([id, text, icon3]) => (0, import_react35.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react35.createElement)(icon3) : null, text)) ); } function Stats({ range, total, previous }) { @@ -13060,25 +17157,25 @@ diff --git a/client.js b/client.js const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null; - const cell = (label, value) => (0, import_react24.createElement)("div", { className: "bt-us-stat" }, (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react24.createElement)("span", { className: "bt-us-stat-value" }, value)); - return (0, import_react24.createElement)( -+ const cell = (label, value) => (0, import_react34.createElement)("div", { className: "bt-us-stat" }, (0, import_react34.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react34.createElement)("span", { className: "bt-us-stat-value" }, value)); -+ return (0, import_react34.createElement)( ++ const cell = (label, value) => (0, import_react35.createElement)("div", { className: "bt-us-stat" }, (0, import_react35.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react35.createElement)("span", { className: "bt-us-stat-value" }, value)); ++ return (0, import_react35.createElement)( "section", { className: "bt-us-stats", style: { "--i": 0 } }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-stat bt-us-stat-main" }, - (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)), ++ (0, import_react35.createElement)( "span", { className: "bt-us-stat-value" }, compact(shown), - (0, import_react24.createElement)("small", null, t("tokens")), - change === null ? null : (0, import_react24.createElement)( -+ (0, import_react34.createElement)("small", null, t("tokens")), -+ change === null ? null : (0, import_react34.createElement)( ++ (0, import_react35.createElement)("small", null, t("tokens")), ++ change === null ? null : (0, import_react35.createElement)( "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` @@ -13087,7 +17184,7 @@ diff --git a/client.js b/client.js } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { - const [hover, setHover] = (0, import_react24.useState)(null); -+ const [hover, setHover] = (0, import_react34.useState)(null); ++ const [hover, setHover] = (0, import_react35.useState)(null); const top = niceTop(Math.max(0, ...columns.map((column) => column.total))); const count = columns.length; const fillOf = new Map(series.map((entry) => [entry.key, entry.fill])); @@ -13095,77 +17192,77 @@ diff --git a/client.js b/client.js const tip = hover === null ? null : columns[hover]; const tipSide = hover !== null && hover >= count / 2 ? { right: `calc(${(count - hover) / count * 100}% + 8px)` } : { left: `calc(${(hover + 1) / count * 100}% + 8px)` }; - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "section", { className: "bt-us-card bt-us-chart", style: { "--i": 1 } }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-card-head" }, - (0, import_react24.createElement)("h3", null, t("Tokens over time"), (0, import_react24.createElement)("span", null, unitLabel(range.unit))), - onBy ? (0, import_react24.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null -+ (0, import_react34.createElement)("h3", null, t("Tokens over time"), (0, import_react34.createElement)("span", null, unitLabel(range.unit))), -+ onBy ? (0, import_react34.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null ++ (0, import_react35.createElement)("h3", null, t("Tokens over time"), (0, import_react35.createElement)("span", null, unitLabel(range.unit))), ++ onBy ? (0, import_react35.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null ), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-chart-body" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) }, - [1, 0.5].map((level) => (0, import_react24.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react24.createElement)("span", null, compact(top * level)))), - (0, import_react24.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react24.createElement)("span", null, "0")), - (0, import_react24.createElement)( -+ [1, 0.5].map((level) => (0, import_react34.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react34.createElement)("span", null, compact(top * level)))), -+ (0, import_react34.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react34.createElement)("span", null, "0")), -+ (0, import_react34.createElement)( ++ [1, 0.5].map((level) => (0, import_react35.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react35.createElement)("span", null, compact(top * level)))), ++ (0, import_react35.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react35.createElement)("span", null, "0")), ++ (0, import_react35.createElement)( "div", { className: "bt-us-cols" }, - columns.map((column, at) => (0, import_react24.createElement)("div", { -+ columns.map((column, at) => (0, import_react34.createElement)("div", { ++ columns.map((column, at) => (0, import_react35.createElement)("div", { key: `${range.from ? `${range.from}~${range.to}` : range.id}:${column.key}`, className: "bt-us-col", "data-on": hover === at ? "" : void 0, onMouseEnter: () => setHover(at) - }, column.total > 0 ? (0, import_react24.createElement)( -+ }, column.total > 0 ? (0, import_react34.createElement)( ++ }, column.total > 0 ? (0, import_react35.createElement)( "div", { className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } }, - column.parts.map(([key, value]) => value > 0 ? (0, import_react24.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) - ) : (0, import_react24.createElement)("div", { className: "bt-us-stub" }))) -+ column.parts.map(([key, value]) => value > 0 ? (0, import_react34.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) -+ ) : (0, import_react34.createElement)("div", { className: "bt-us-stub" }))) ++ column.parts.map(([key, value]) => value > 0 ? (0, import_react35.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null) ++ ) : (0, import_react35.createElement)("div", { className: "bt-us-stub" }))) ), - tip ? (0, import_react24.createElement)( -+ tip ? (0, import_react34.createElement)( ++ tip ? (0, import_react35.createElement)( "div", { className: "bt-us-tip", style: tipSide }, - (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, tip.title), - (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), - tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-tip-title" }, tip.title), -+ (0, import_react34.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), -+ tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-tip-title" }, tip.title), ++ (0, import_react35.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(tip.total) })), ++ tip.tip.filter(([, value]) => value > 0).sort((a, b) => b[1] - a[1]).map(([key, value]) => (0, import_react35.createElement)( "div", { key, className: "bt-us-tip-row" }, - (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), - (0, import_react24.createElement)("span", null, tipOf.get(key)?.name), - (0, import_react24.createElement)("b", null, compact(value)) -+ (0, import_react34.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), -+ (0, import_react34.createElement)("span", null, tipOf.get(key)?.name), -+ (0, import_react34.createElement)("b", null, compact(value)) ++ (0, import_react35.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }), ++ (0, import_react35.createElement)("span", null, tipOf.get(key)?.name), ++ (0, import_react35.createElement)("b", null, compact(value)) )) ) : null ), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-ticks", "aria-hidden": true }, // A preset counts its labels back from now; a picked span forward from its first day. - columns.map((column, at) => (0, import_react24.createElement)( -+ columns.map((column, at) => (0, import_react34.createElement)( ++ columns.map((column, at) => (0, import_react35.createElement)( "span", { key: column.key, "data-now": at === count - 1 && range.ends !== false ? "" : void 0 }, (range.from ? at : count - 1 - at) % range.stride === 0 ? column.tick : "" @@ -13173,23 +17270,23 @@ diff --git a/client.js b/client.js ) ), - series.length > 1 || series[0]?.pick ? (0, import_react24.createElement)( -+ series.length > 1 || series[0]?.pick ? (0, import_react34.createElement)( ++ series.length > 1 || series[0]?.pick ? (0, import_react35.createElement)( "div", { className: "bt-us-legend" }, - series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", { -+ series.map((entry) => (0, import_react34.createElement)(entry.pick ? "button" : "span", { ++ series.map((entry) => (0, import_react35.createElement)(entry.pick ? "button" : "span", { key: entry.key, className: "bt-us-key", ...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {} - }, (0, import_react24.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) -+ }, (0, import_react34.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) ++ }, (0, import_react35.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name)) ) : null ); } function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick }) { const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0; - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( onClick ? "button" : "div", { className: "bt-us-row", @@ -13199,77 +17296,77 @@ diff --git a/client.js b/client.js }, - glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, - (0, import_react24.createElement)( -+ glyph ? (0, import_react34.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, -+ (0, import_react34.createElement)( ++ glyph ? (0, import_react35.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null, ++ (0, import_react35.createElement)( "span", { className: "bt-us-row-body" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "span", { className: "bt-us-row-line" }, - (0, import_react24.createElement)("span", { className: "bt-us-row-name" }, name), - sub ? (0, import_react24.createElement)("span", { className: "bt-us-row-sub" }, sub) : null -+ (0, import_react34.createElement)("span", { className: "bt-us-row-name" }, name), -+ sub ? (0, import_react34.createElement)("span", { className: "bt-us-row-sub" }, sub) : null ++ (0, import_react35.createElement)("span", { className: "bt-us-row-name" }, name), ++ sub ? (0, import_react35.createElement)("span", { className: "bt-us-row-sub" }, sub) : null ), - (0, import_react24.createElement)("span", { className: "bt-us-track" }, (0, import_react24.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) -+ (0, import_react34.createElement)("span", { className: "bt-us-track" }, (0, import_react34.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) ++ (0, import_react35.createElement)("span", { className: "bt-us-track" }, (0, import_react35.createElement)("span", { style: { width: `${width}%`, background: fill2 } })) ), - (0, import_react24.createElement)("span", { className: "bt-us-row-num" }, (0, import_react24.createElement)("b", null, compact(sum.total)), (0, import_react24.createElement)("small", null, `${percent(sum.total, whole)}%`)) -+ (0, import_react34.createElement)("span", { className: "bt-us-row-num" }, (0, import_react34.createElement)("b", null, compact(sum.total)), (0, import_react34.createElement)("small", null, `${percent(sum.total, whole)}%`)) ++ (0, import_react35.createElement)("span", { className: "bt-us-row-num" }, (0, import_react35.createElement)("b", null, compact(sum.total)), (0, import_react35.createElement)("small", null, `${percent(sum.total, whole)}%`)) ); } function ListCard({ title, count, index, children }) { - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "section", { className: "bt-us-card", style: { "--i": index } }, - (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, title, count === void 0 ? null : (0, import_react24.createElement)("span", null, count))), - (0, import_react24.createElement)("div", { className: "bt-us-rows" }, children) -+ (0, import_react34.createElement)("div", { className: "bt-us-card-head" }, (0, import_react34.createElement)("h3", null, title, count === void 0 ? null : (0, import_react34.createElement)("span", null, count))), -+ (0, import_react34.createElement)("div", { className: "bt-us-rows" }, children) ++ (0, import_react35.createElement)("div", { className: "bt-us-card-head" }, (0, import_react35.createElement)("h3", null, title, count === void 0 ? null : (0, import_react35.createElement)("span", null, count))), ++ (0, import_react35.createElement)("div", { className: "bt-us-rows" }, children) ); } function Profile({ face, share, range }) { - const arc = Math.max(0, Math.min(1, share)) * RING; - return (0, import_react24.createElement)( + const arc = Math.max(0, Math.min(1, share)) * RING2; -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "section", { className: "bt-us-profile" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "span", { className: "bt-us-ring" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "svg", { viewBox: "0 0 96 96", "aria-hidden": true }, - face.stops ? (0, import_react24.createElement)("defs", null, (0, import_react24.createElement)( -+ face.stops ? (0, import_react34.createElement)("defs", null, (0, import_react34.createElement)( ++ face.stops ? (0, import_react35.createElement)("defs", null, (0, import_react35.createElement)( "linearGradient", { id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 }, - face.stops.map(([offset, color]) => (0, import_react24.createElement)("stop", { key: offset, offset, stopColor: color })) -+ face.stops.map(([offset, color]) => (0, import_react34.createElement)("stop", { key: offset, offset, stopColor: color })) ++ face.stops.map(([offset, color]) => (0, import_react35.createElement)("stop", { key: offset, offset, stopColor: color })) )) : null, - (0, import_react24.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), - arc > 0 ? (0, import_react24.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING}` } }) : null -+ (0, import_react34.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), -+ arc > 0 ? (0, import_react34.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null ++ (0, import_react35.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }), ++ arc > 0 ? (0, import_react35.createElement)("circle", { className: "bt-us-ring-arc", cx: 48, cy: 48, r: RING_R, style: { stroke: face.stops ? "url(#bt-us-ring-paint)" : face.paint, strokeDasharray: `${arc} ${RING2}` } }) : null ), - (0, import_react24.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) -+ (0, import_react34.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) ++ (0, import_react35.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64)) ), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-profile-copy" }, - (0, import_react24.createElement)("h3", null, face.name), - face.sub ? (0, import_react24.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, - (0, import_react24.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) -+ (0, import_react34.createElement)("h3", null, face.name), -+ face.sub ? (0, import_react34.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, -+ (0, import_react34.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) ++ (0, import_react35.createElement)("h3", null, face.name), ++ face.sub ? (0, import_react35.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null, ++ (0, import_react35.createElement)("span", { className: "bt-us-profile-share" }, range.from ? t("{share}% of all usage from {span}", { share: percent(share, 1), span: spanText(range.from, range.to) }) : t("{share}% of all usage in the last {range}", { share: percent(share, 1), range: rangeLabel(range.id) })) ) ); } @@ -13277,17 +17374,17 @@ diff --git a/client.js b/client.js var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1); function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { - const box = (0, import_react24.useRef)(null); -+ const box = (0, import_react34.useRef)(null); ++ const box = (0, import_react35.useRef)(null); const todayMs = hourMs(today); const thisMonth = monthOf(todayMs); - const [right, setRight] = (0, import_react24.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); - const [anchor, setAnchor] = (0, import_react24.useState)(null); - const [hover, setHover] = (0, import_react24.useState)(null); - (0, import_react24.useEffect)(() => { -+ const [right, setRight] = (0, import_react34.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); -+ const [anchor, setAnchor] = (0, import_react34.useState)(null); -+ const [hover, setHover] = (0, import_react34.useState)(null); -+ (0, import_react34.useEffect)(() => { ++ const [right, setRight] = (0, import_react35.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today)))); ++ const [anchor, setAnchor] = (0, import_react35.useState)(null); ++ const [hover, setHover] = (0, import_react35.useState)(null); ++ (0, import_react35.useEffect)(() => { const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; @@ -13296,19 +17393,19 @@ diff --git a/client.js b/client.js onPick(from, to); }; - const weekdays = (0, import_react24.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); -+ const weekdays = (0, import_react34.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); ++ const weekdays = (0, import_react35.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []); const month = (start, side) => { const length = new Date(addMonths(start, 1) - DAY_MS).getUTCDate(); const lead = (new Date(start).getUTCDay() + 6) % 7; - const cells = Array.from({ length: lead }, (_, at) => (0, import_react24.createElement)("span", { key: `b${at}` })); -+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react34.createElement)("span", { key: `b${at}` })); ++ const cells = Array.from({ length: lead }, (_, at) => (0, import_react35.createElement)("span", { key: `b${at}` })); for (let day = 1; day <= length; day += 1) { const ms = start + (day - 1) * DAY_MS; const key = dayKey(ms); const weekday = (lead + day - 1) % 7; const flag = (on) => on ? "" : void 0; - cells.push((0, import_react24.createElement)("button", { -+ cells.push((0, import_react34.createElement)("button", { ++ cells.push((0, import_react35.createElement)("button", { key, type: "button", className: "bt-us-day", @@ -13317,20 +17414,20 @@ diff --git a/client.js b/client.js onMouseEnter: () => setHover(key), onClick: () => press(key) - }, (0, import_react24.createElement)("b", null, day))); -+ }, (0, import_react34.createElement)("b", null, day))); ++ }, (0, import_react35.createElement)("b", null, day))); } - const nav = side === "left" ? (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react24.createElement)(ChevronLeftIcon)) : (0, import_react24.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react24.createElement)(ChevronRightIcon)); - return (0, import_react24.createElement)( -+ const nav = side === "left" ? (0, import_react34.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react34.createElement)(ChevronLeftIcon)) : (0, import_react34.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react34.createElement)(ChevronRightIcon)); -+ return (0, import_react34.createElement)( ++ const nav = side === "left" ? (0, import_react35.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "left", "aria-label": t("Previous month"), disabled: start <= oldest, onClick: () => setRight(addMonths(right, -1)) }, (0, import_react35.createElement)(ChevronLeftIcon)) : (0, import_react35.createElement)("button", { type: "button", className: "bt-us-month-nav", "data-side": "right", "aria-label": t("Next month"), disabled: start >= thisMonth, onClick: () => setRight(addMonths(right, 1)) }, (0, import_react35.createElement)(ChevronRightIcon)); ++ return (0, import_react35.createElement)( "div", { key: start, className: "bt-us-month" }, - (0, import_react24.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react24.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), - (0, import_react24.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react24.createElement)("span", { key: at }, name))), - (0, import_react24.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) -+ (0, import_react34.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react34.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), -+ (0, import_react34.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react34.createElement)("span", { key: at }, name))), -+ (0, import_react34.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) ++ (0, import_react35.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react35.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))), ++ (0, import_react35.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react35.createElement)("span", { key: at }, name))), ++ (0, import_react35.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells) ); }; const shortcuts = [ @@ -13339,15 +17436,15 @@ diff --git a/client.js b/client.js ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "div", { ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-pick-quick" }, - shortcuts.map(([id, label, from, to]) => (0, import_react24.createElement)("button", { -+ shortcuts.map(([id, label, from, to]) => (0, import_react34.createElement)("button", { ++ shortcuts.map(([id, label, from, to]) => (0, import_react35.createElement)("button", { key: id, type: "button", "aria-pressed": span?.from === from && span?.to === to, @@ -13356,20 +17453,20 @@ diff --git a/client.js b/client.js ), - (0, import_react24.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")), ++ (0, import_react35.createElement)( "div", { className: "bt-us-pick-foot", role: "status" }, - (0, import_react24.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), - low === null ? null : (0, import_react24.createElement)("span", null, (0, import_react24.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) -+ (0, import_react34.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), -+ low === null ? null : (0, import_react34.createElement)("span", null, (0, import_react34.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) ++ (0, import_react35.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")), ++ low === null ? null : (0, import_react35.createElement)("span", null, (0, import_react35.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`) ) ); } function Heat({ view, keep, paint, index }) { - const [hover, setHover] = (0, import_react24.useState)(null); -+ const [hover, setHover] = (0, import_react34.useState)(null); ++ const [hover, setHover] = (0, import_react35.useState)(null); const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); @@ -13378,14 +17475,14 @@ diff --git a/client.js b/client.js const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { - months.push((0, import_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); -+ months.push((0, import_react34.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); ++ months.push((0, import_react35.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday))); } for (let day = 0; day < 7; day += 1) { const ms = monday + day * DAY_MS; const value = daily.get(dayKey(ms)) ?? 0; const level = value > 0 ? Math.max(1, Math.ceil(4 * Math.sqrt(value / most))) : 0; - cells.push((0, import_react24.createElement)("span", { -+ cells.push((0, import_react34.createElement)("span", { ++ cells.push((0, import_react35.createElement)("span", { key: ms, className: "bt-us-cell", style: { "--w": week }, @@ -13394,7 +17491,7 @@ diff --git a/client.js b/client.js "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) - }, level > 0 ? (0, import_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); -+ }, level > 0 ? (0, import_react34.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); ++ }, level > 0 ? (0, import_react35.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null)); } } const tipSide = hover ? { @@ -13403,63 +17500,63 @@ diff --git a/client.js b/client.js const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "section", { className: "bt-us-card bt-us-heat", style: { "--i": index } }, - (0, import_react24.createElement)("div", { className: "bt-us-card-head" }, (0, import_react24.createElement)("h3", null, t("Activity")), (0, import_react24.createElement)("span", { className: "bt-us-head-note" }, summary)), - (0, import_react24.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-card-head" }, (0, import_react34.createElement)("h3", null, t("Activity")), (0, import_react34.createElement)("span", { className: "bt-us-head-note" }, summary)), -+ (0, import_react34.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-card-head" }, (0, import_react35.createElement)("h3", null, t("Activity")), (0, import_react35.createElement)("span", { className: "bt-us-head-note" }, summary)), ++ (0, import_react35.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months), ++ (0, import_react35.createElement)( "div", { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) }, - (0, import_react24.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), - hover ? (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), -+ hover ? (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells), ++ hover ? (0, import_react35.createElement)( "div", { className: "bt-us-tip bt-us-heat-tip", style: tipSide }, - (0, import_react24.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), - (0, import_react24.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) -+ (0, import_react34.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), -+ (0, import_react34.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) ++ (0, import_react35.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)), ++ (0, import_react35.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) })) ) : null ), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-heat-scale", "aria-hidden": true }, t("Light"), - levels.map((level) => (0, import_react24.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react24.createElement)("i", { style: { background: soften(paint, level) } }))), -+ levels.map((level) => (0, import_react34.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react34.createElement)("i", { style: { background: soften(paint, level) } }))), ++ levels.map((level) => (0, import_react35.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react35.createElement)("i", { style: { background: soften(paint, level) } }))), t("Heavy") ) ); } function Skeleton2() { - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "div", { className: "bt-us", "aria-busy": true }, - (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }), ++ (0, import_react35.createElement)( "div", { className: "bt-us-sheet bt-us-skel-sheet" }, - (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), - (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), -+ (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }), ++ (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }), ++ (0, import_react35.createElement)( "div", { className: "bt-us-pair" }, - (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), - (0, import_react24.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) -+ (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), -+ (0, import_react34.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) ++ (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), ++ (0, import_react35.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }) ) ) ); @@ -13473,16 +17570,16 @@ diff --git a/client.js b/client.js - const [picking, setPicking] = (0, import_react24.useState)(false); - const [by, setBy] = (0, import_react24.useState)("bot"); - const tabs = (0, import_react24.useRef)(null); -+ const [view, setView] = (0, import_react34.useState)(null); -+ const [error, setError] = (0, import_react34.useState)(""); -+ const [rangeId, setRangeId] = (0, import_react34.useState)("7d"); -+ const [span, setSpan] = (0, import_react34.useState)(null); -+ const [picking, setPicking] = (0, import_react34.useState)(false); -+ const [by, setBy] = (0, import_react34.useState)("bot"); -+ const tabs = (0, import_react34.useRef)(null); ++ const [view, setView] = (0, import_react35.useState)(null); ++ const [error, setError] = (0, import_react35.useState)(""); ++ const [rangeId, setRangeId] = (0, import_react35.useState)("7d"); ++ const [span, setSpan] = (0, import_react35.useState)(null); ++ const [picking, setPicking] = (0, import_react35.useState)(false); ++ const [by, setBy] = (0, import_react35.useState)("bot"); ++ const tabs = (0, import_react35.useRef)(null); const focus = focusOf(focusId); - (0, import_react24.useEffect)(() => { -+ (0, import_react34.useEffect)(() => { ++ (0, import_react35.useEffect)(() => { let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { @@ -13491,7 +17588,7 @@ diff --git a/client.js b/client.js await resized; if (stopped) return; - (0, import_react24.startTransition)(() => setView(value)); -+ (0, import_react34.startTransition)(() => setView(value)); ++ (0, import_react35.startTransition)(() => setView(value)); setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { @@ -13500,7 +17597,7 @@ diff --git a/client.js b/client.js const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); - const shaped = (0, import_react24.useMemo)(() => { -+ const shaped = (0, import_react34.useMemo)(() => { ++ const shaped = (0, import_react35.useMemo)(() => { if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); @@ -13509,7 +17606,7 @@ diff --git a/client.js b/client.js const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; - return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react24.createElement)(ModelGlyph, { name, paint, size }) }; -+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react34.createElement)(ModelGlyph, { name, paint, size }) }; ++ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react35.createElement)(ModelGlyph, { name, paint, size }) }; }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; @@ -13519,29 +17616,29 @@ diff --git a/client.js b/client.js }, [view, rangeId, span, by, focusId, roster]); - const root = (0, import_react24.useRef)(null); - (0, import_react24.useEffect)(() => { -+ const root = (0, import_react34.useRef)(null); -+ (0, import_react34.useEffect)(() => { ++ const root = (0, import_react35.useRef)(null); ++ (0, import_react35.useEffect)(() => { root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 }); }, [focusId]); if (!view) { - return error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react24.createElement)(Skeleton2); -+ return error ? (0, import_react34.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react34.createElement)(Skeleton2); ++ return error ? (0, import_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react35.createElement)(Skeleton2); } if (view.rows.length === 0) { - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "div", { className: "bt-us" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-empty" }, - (0, import_react24.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), - (0, import_react24.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), - (0, import_react24.createElement)("span", null, t("Token usage shows up here after a model is called.")) -+ (0, import_react34.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), -+ (0, import_react34.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), -+ (0, import_react34.createElement)("span", null, t("Token usage shows up here after a model is called.")) ++ (0, import_react35.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }), ++ (0, import_react35.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")), ++ (0, import_react35.createElement)("span", null, t("Token usage shows up here after a model is called.")) ) ); } @@ -13550,7 +17647,7 @@ diff --git a/client.js b/client.js ]; const whole = part.total.total; - const ownerRow = ([at, sum], index, total = whole) => (0, import_react24.createElement)(Row2, { -+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react34.createElement)(Row2, { ++ const ownerRow = ([at, sum], index, total = whole) => (0, import_react35.createElement)(Row2, { key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), @@ -13559,7 +17656,7 @@ diff --git a/client.js b/client.js onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); - const modelRow = ([at, sum], index) => (0, import_react24.createElement)(Row2, { -+ const modelRow = ([at, sum], index) => (0, import_react34.createElement)(Row2, { ++ const modelRow = ([at, sum], index) => (0, import_react35.createElement)(Row2, { key: `m${at}`, kind: "model", glyph: models[at].glyph(32), @@ -13568,7 +17665,7 @@ diff --git a/client.js b/client.js onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); - const nothing = (0, import_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); -+ const nothing = (0, import_react34.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); ++ const nothing = (0, import_react35.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range.")); let body; if (face) { const kinds = ranked(part.kinds); @@ -13577,30 +17674,30 @@ diff --git a/client.js b/client.js body = [ - (0, import_react24.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }), ++ (0, import_react35.createElement)( "div", { key: "sheet", className: "bt-us-sheet" }, - (0, import_react24.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), - (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), -+ (0, import_react34.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }), ++ (0, import_react35.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }), ++ (0, import_react35.createElement)( "div", { key: "pair", className: "bt-us-pair" }, - ownerAt >= 0 ? (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react24.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), - (0, import_react24.createElement)( -+ ownerAt >= 0 ? (0, import_react34.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react34.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), -+ (0, import_react34.createElement)( ++ ownerAt >= 0 ? (0, import_react35.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react35.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing), ++ (0, import_react35.createElement)( ListCard, { title: t("What for"), index: 3 }, - kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react24.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing -+ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react34.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing ++ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react35.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing ) ), - (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) -+ (0, import_react34.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) ++ (0, import_react35.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 }) ) ]; } else { @@ -13609,52 +17706,52 @@ diff --git a/client.js b/client.js const usedModels = ranked(all.models); body = [ - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { key: "sheet", className: "bt-us-sheet" }, - (0, import_react24.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), - (0, import_react24.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), - (0, import_react24.createElement)( -+ (0, import_react34.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), -+ (0, import_react34.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }), ++ (0, import_react35.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }), ++ (0, import_react35.createElement)( "div", { key: "pair", className: "bt-us-pair" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( ListCard, { title: t("Bots"), count: bots.length, index: 2 }, owners.length > 0 ? null : nothing, bots.map((entry, index) => ownerRow(entry, index)), - bots.length > 0 && rest.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, -+ bots.length > 0 && rest.length > 0 ? (0, import_react34.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, ++ bots.length > 0 && rest.length > 0 ? (0, import_react35.createElement)("div", { className: "bt-us-split", role: "separator" }) : null, rest.map((entry, index) => ownerRow(entry, bots.length + index)) ), - (0, import_react24.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) -+ (0, import_react34.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) ++ (0, import_react35.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) ), - (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) -+ (0, import_react34.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) ++ (0, import_react35.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 }) ) ]; } - return (0, import_react24.createElement)( -+ return (0, import_react34.createElement)( ++ return (0, import_react35.createElement)( "div", { ref: root, className: "bt-us", key: focusId ?? "all" }, - (0, import_react24.createElement)( -+ (0, import_react34.createElement)( ++ (0, import_react35.createElement)( "div", { className: "bt-us-top" }, - (0, import_react24.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), - view.scanning ? (0, import_react24.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, - picking ? (0, import_react24.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null -+ (0, import_react34.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), -+ view.scanning ? (0, import_react34.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, -+ picking ? (0, import_react34.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null ++ (0, import_react35.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }), ++ view.scanning ? (0, import_react35.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null, ++ picking ? (0, import_react35.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null ), - error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, -+ error ? (0, import_react34.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, ++ error ? (0, import_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null, body ); } @@ -13671,7 +17768,7 @@ diff --git a/client.js b/client.js // src/client/connectors.js -var import_react25 = require("react"); -+var import_react35 = require("react"); ++var import_react36 = require("react"); var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new"; -var GitHubMark = () => (0, import_react25.createElement)( +var COMPOSIO_KEY_URL = "https://platform.composio.dev/"; @@ -13705,11 +17802,11 @@ diff --git a/client.js b/client.js + ["youtube", "YouTube"] +]; +var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug; -+var GitHubMark = () => (0, import_react35.createElement)( ++var GitHubMark = () => (0, import_react36.createElement)( "svg", { width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true }, - (0, import_react25.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) -+ (0, import_react35.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) ++ (0, import_react36.createElement)("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" }) ); -var MARKS = { github: GitHubMark }; -var ABOUT = { github: () => t("Repositories, issues, and pull requests") }; @@ -13733,20 +17830,20 @@ diff --git a/client.js b/client.js +var APP_STATUS = { ready: () => t("Ready"), "sign-in": () => t("Needs sign-in") }; +function AppChip({ name, status, onRemove }) { + const label = APP_STATUS[status]?.(); -+ return (0, import_react35.createElement)( ++ return (0, import_react36.createElement)( + "button", + { type: "button", className: "bt-state bt-app-chip", "aria-pressed": true, "data-status": status ?? "unknown", title: t("Remove {app}", { app: name }), onClick: onRemove }, + name, -+ label ? (0, import_react35.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null ++ label ? (0, import_react36.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null + ); +} +function AppPicker({ apps, onChange, search, statuses = {} }) { -+ const [query, setQuery] = (0, import_react35.useState)(""); -+ const [found, setFound] = (0, import_react35.useState)(null); -+ const [failed, setFailed] = (0, import_react35.useState)(null); -+ const names = (0, import_react35.useRef)(new Map(APPS)); -+ const asked = (0, import_react35.useRef)(0); -+ (0, import_react35.useEffect)(() => { ++ const [query, setQuery] = (0, import_react36.useState)(""); ++ const [found, setFound] = (0, import_react36.useState)(null); ++ const [failed, setFailed] = (0, import_react36.useState)(null); ++ const names = (0, import_react36.useRef)(new Map(APPS)); ++ const asked = (0, import_react36.useRef)(0); ++ (0, import_react36.useEffect)(() => { + const text = query.trim(); + if (text === "" || search === void 0) { + setFound(null); @@ -13773,13 +17870,13 @@ diff --git a/client.js b/client.js + const needle = query.trim().toLowerCase(); + const local = APPS.filter(([slug, name]) => needle === "" || slug.includes(needle) || name.toLowerCase().includes(needle)).map(([slug, name]) => ({ slug, name })); + const results = (found ?? local).filter((app) => !picked.has(app.slug)); -+ return (0, import_react35.createElement)( ++ return (0, import_react36.createElement)( + "div", + { className: "bt-connector-apps" }, -+ apps.length > 0 ? (0, import_react35.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react35.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, -+ (0, import_react35.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), -+ failed ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, -+ (0, import_react35.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react35.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { ++ apps.length > 0 ? (0, import_react36.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react36.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null, ++ (0, import_react36.createElement)("input", { className: "bt-input", type: "search", value: query, spellCheck: false, "aria-label": t("Search apps"), placeholder: t("Search Composio apps"), onChange: (event) => setQuery(event.target.value) }), ++ failed ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null, ++ (0, import_react36.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react36.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => { + names.current.set(app.slug, app.name); + onChange([...apps, app.slug]); + } }, app.name))) @@ -13787,21 +17884,21 @@ diff --git a/client.js b/client.js +} +function AppStatusList({ apps, statuses }) { + if (apps.length === 0 || Object.keys(statuses).length === 0) return null; -+ return (0, import_react35.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react35.createElement)( ++ return (0, import_react36.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react36.createElement)( + "li", + { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" }, -+ (0, import_react35.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), -+ (0, import_react35.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") ++ (0, import_react36.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)), ++ (0, import_react36.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "") + ))); +} +function ConnectorRow({ connector, roster, actions }) { -+ const [form, setForm] = (0, import_react35.useState)(false); -+ const [bots, setBots] = (0, import_react35.useState)(false); -+ const [token, setToken] = (0, import_react35.useState)(""); -+ const [apps, setApps] = (0, import_react35.useState)(() => connector.apps ?? []); -+ const [picking, setPicking] = (0, import_react35.useState)(false); -+ const [busy, setBusy] = (0, import_react35.useState)(false); -+ const [error, setError] = (0, import_react35.useState)(null); ++ const [form, setForm] = (0, import_react36.useState)(false); ++ const [bots, setBots] = (0, import_react36.useState)(false); ++ const [token, setToken] = (0, import_react36.useState)(""); ++ const [apps, setApps] = (0, import_react36.useState)(() => connector.apps ?? []); ++ const [picking, setPicking] = (0, import_react36.useState)(false); ++ const [busy, setBusy] = (0, import_react36.useState)(false); ++ const [error, setError] = (0, import_react36.useState)(null); const run = (task, after) => { setBusy(true); setError(null); @@ -13810,9 +17907,9 @@ diff --git a/client.js b/client.js }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; + const gateway = connector.id === "composio"; -+ const [statuses, setStatuses] = (0, import_react35.useState)({}); ++ const [statuses, setStatuses] = (0, import_react36.useState)({}); + const appsKey = (connector.apps ?? []).join(","); -+ (0, import_react35.useEffect)(() => { ++ (0, import_react36.useEffect)(() => { + if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) { + setStatuses({}); + return void 0; @@ -13827,7 +17924,7 @@ diff --git a/client.js b/client.js + live = false; + }; + }, [gateway, connector.state, appsKey, picking]); -+ const search = (0, import_react35.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); ++ const search = (0, import_react36.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]); const connect = () => { if (busy || token.trim() === "") return; - void run(actions.connectorConnect(connector.id, token), () => { @@ -13853,109 +17950,109 @@ diff --git a/client.js b/client.js - const row = (0, import_react25.useRef)(null); - useMorph(row, `${form}:${error !== null}:${connector.state}`); - return (0, import_react25.createElement)( -+ const row = (0, import_react35.useRef)(null); ++ const row = (0, import_react36.useRef)(null); + useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`); -+ return (0, import_react35.createElement)( ++ return (0, import_react36.createElement)( "div", { ref: row, className: "bt-connector", "data-state": connector.state }, - (0, import_react25.createElement)( -+ (0, import_react35.createElement)( ++ (0, import_react36.createElement)( "div", { className: "bt-connector-head" }, - (0, import_react25.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react25.createElement)(Mark) : null), - (0, import_react25.createElement)( -+ (0, import_react35.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react35.createElement)(Mark) : null), -+ (0, import_react35.createElement)( ++ (0, import_react36.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react36.createElement)(Mark) : null), ++ (0, import_react36.createElement)( "span", { className: "bt-connector-text" }, - (0, import_react25.createElement)("span", { className: "bt-connector-name" }, connector.name), - (0, import_react25.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) -+ (0, import_react35.createElement)("span", { className: "bt-connector-name" }, connector.name), -+ (0, import_react35.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) ++ (0, import_react36.createElement)("span", { className: "bt-connector-name" }, connector.name), ++ (0, import_react36.createElement)("span", { className: "bt-connector-status" }, statusText2(connector)) ), - (0, import_react25.createElement)( -+ (0, import_react35.createElement)( ++ (0, import_react36.createElement)( "span", { className: "bt-connector-actions" }, - connector.state === "error" ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, - on ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) -+ connector.state === "error" ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, -+ on && gateway && !picking ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ connector.state === "error" ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorRetry(connector.id)) }, t("Try again")) : null, ++ on && gateway && !picking ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + setApps(connector.apps ?? []); + setPicking(true); + } }, t("Choose apps")) : null, -+ on ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, -+ on ? (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) ++ on ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-expanded": bots, onClick: () => setBots((value) => !value) }, bots ? t("Done") : t("Choose Bots")) : null, ++ on ? (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorDisconnect(connector.id)) }, t("Disconnect")) : form ? null : (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect")) ) ), - !on && form ? (0, import_react25.createElement)( -+ on && gateway && !picking ? (0, import_react35.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, -+ on ? (0, import_react35.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, -+ on && bots ? (0, import_react35.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, -+ !on && form ? (0, import_react35.createElement)( ++ on && gateway && !picking ? (0, import_react36.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null, ++ on ? (0, import_react36.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null, ++ on && bots ? (0, import_react36.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null, ++ !on && form ? (0, import_react36.createElement)( "div", { className: "bt-connector-form" }, - (0, import_react25.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react25.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: t("GitHub token"), autoFocus: true })), - (0, import_react25.createElement)( -+ (0, import_react35.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react35.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), -+ gateway ? (0, import_react35.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, -+ (0, import_react35.createElement)( ++ (0, import_react36.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react36.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), ++ gateway ? (0, import_react36.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, ++ (0, import_react36.createElement)( "div", { className: "bt-connector-form-row" }, - (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), - (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { -+ (0, import_react35.createElement)("a", { className: "bt-connector-link", href: gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, gateway ? t("Get a Composio API key") : t("Create a token on GitHub")), -+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ (0, import_react36.createElement)("a", { className: "bt-connector-link", href: gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, gateway ? t("Get a Composio API key") : t("Create a token on GitHub")), ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { setForm(false); setToken(""); 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_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) - ) - ) : null, -- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null -+ on && gateway && picking ? (0, import_react35.createElement)( ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) ++ ) ++ ) : null, ++ on && gateway && picking ? (0, import_react36.createElement)( + "div", + { className: "bt-connector-form" }, -+ (0, import_react35.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), -+ (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), -+ (0, import_react35.createElement)( ++ (0, import_react36.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }), ++ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("The first time a Bot uses an app, it shows a sign-in link in the chat.")), ++ (0, import_react36.createElement)( + "div", + { className: "bt-connector-form-row" }, -+ (0, import_react35.createElement)("span", { className: "bt-connector-link" }), -+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ (0, import_react36.createElement)("span", { className: "bt-connector-link" }), ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + setPicking(false); + setError(null); + } }, t("Cancel")), -+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) -+ ) -+ ) : null, -+ error ? (0, import_react35.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) + ) + ) : null, +- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ++ error ? (0, import_react36.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ); } function ConnectorsPage({ roster, actions }) { const list = roster?.connectors ?? []; - return (0, import_react25.createElement)( -+ return (0, import_react35.createElement)( ++ return (0, import_react36.createElement)( "section", { className: "bt-set-section" }, - (0, import_react25.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react25.createElement)(ConnectorRow, { key: connector.id, connector, actions }))) -+ (0, import_react35.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react35.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) ++ (0, import_react36.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react36.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions }))) ); } // src/client/feedback.js -var import_react26 = require("react"); -var FeedbackIcon = () => (0, import_react26.createElement)( -+var import_react36 = require("react"); -+var FeedbackIcon = () => (0, import_react36.createElement)( ++var import_react37 = require("react"); ++var FeedbackIcon = () => (0, import_react37.createElement)( "svg", { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, - (0, import_react26.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), - (0, import_react26.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) -+ (0, import_react36.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), -+ (0, import_react36.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) ++ (0, import_react37.createElement)("path", { d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-3.5 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }), ++ (0, import_react37.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" }) ); function download(name, text) { const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); @@ -13979,17 +18076,17 @@ diff --git a/client.js b/client.js - const [report, setReport] = (0, import_react26.useState)(null); - const [preview, setPreview] = (0, import_react26.useState)(false); - const [note, setNote] = (0, import_react26.useState)(""); -+ const [what, setWhat] = (0, import_react36.useState)(""); -+ const [report, setReport] = (0, import_react36.useState)(null); -+ const [preview, setPreview] = (0, import_react36.useState)(false); -+ const [note, setNote] = (0, import_react36.useState)(""); ++ const [what, setWhat] = (0, import_react37.useState)(""); ++ const [report, setReport] = (0, import_react37.useState)(null); ++ const [preview, setPreview] = (0, import_react37.useState)(false); ++ const [note, setNote] = (0, import_react37.useState)(""); const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => { setReport(next); return next; }); const load = () => fetchReport().then(() => void 0); - (0, import_react26.useEffect)(() => { -+ (0, import_react36.useEffect)(() => { ++ (0, import_react37.useEffect)(() => { void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { @@ -13998,7 +18095,7 @@ diff --git a/client.js b/client.js if (!copied) setPreview(true); })); - const [issueLink, setIssueLink] = (0, import_react26.useState)(null); -+ const [issueLink, setIssueLink] = (0, import_react36.useState)(null); ++ const [issueLink, setIssueLink] = (0, import_react37.useState)(null); const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; @@ -14007,83 +18104,83 @@ diff --git a/client.js b/client.js const shown = entries2.slice(0, 8); return [ - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( Section, { key: "report", title: t("Report a problem") }, - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( "div", { className: "bt-set-row bt-set-paste bt-fb-what" }, - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( "label", { className: "bt-set-copy", htmlFor: "bt-fb-what" }, - (0, import_react26.createElement)("span", null, t("What happened?")), - (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) -+ (0, import_react36.createElement)("span", null, t("What happened?")), -+ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) ++ (0, import_react37.createElement)("span", null, t("What happened?")), ++ (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue.")) ), - (0, import_react26.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) -+ (0, import_react36.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) ++ (0, import_react37.createElement)("textarea", { id: "bt-fb-what", className: "bt-input bt-input-area", value: what, maxLength: WHAT_MAX, placeholder: t("For example: I asked Writer in the group chat and it showed an error."), onChange: (event) => setWhat(event.target.value) }) ), - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( Row3, { label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") }, - (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), - (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")), ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy")) ), - preview ? (0, import_react26.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react26.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, - (0, import_react26.createElement)( -+ preview ? (0, import_react36.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react36.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, -+ (0, import_react36.createElement)( ++ preview ? (0, import_react37.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react37.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null, ++ (0, import_react37.createElement)( Row3, - { label: t("GitHub issue"), hint: t("Opens a new issue in the DS Bot repository with all of this filled in. Nothing is sent until you submit it there.") }, - (0, import_react26.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) + { label: t("GitHub issue"), hint: t("Opens a new issue in the FlyBot repository with all of this filled in. Nothing is sent until you submit it there.") }, -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub")) ) ), - note ? (0, import_react26.createElement)( -+ note ? (0, import_react36.createElement)( ++ note ? (0, import_react37.createElement)( "div", { key: "note", className: "bt-note bt-fb-note", role: "status" }, note, - issueLink ? [" ", (0, import_react26.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null -+ issueLink ? [" ", (0, import_react36.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null ++ issueLink ? [" ", (0, import_react37.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null ) : null, - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( Section, { key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") }, - shown.length === 0 ? (0, import_react26.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and DS Bot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react26.createElement)( -+ shown.length === 0 ? (0, import_react36.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react36.createElement)( ++ shown.length === 0 ? (0, import_react37.createElement)(Row3, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react37.createElement)( "div", { key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text }, - (0, import_react26.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), - (0, import_react26.createElement)( -+ (0, import_react36.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }), ++ (0, import_react37.createElement)( "div", { className: "bt-set-copy" }, - (0, import_react26.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), - (0, import_react26.createElement)("span", { className: "bt-set-hint" }, [whenText(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "DS Bot")].filter(Boolean).join(" · ")) -+ (0, import_react36.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), -+ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) ++ (0, import_react37.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)), ++ (0, import_react37.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · ")) ), - entry.code ? (0, import_react26.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null -+ entry.code ? (0, import_react36.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null ++ entry.code ? (0, import_react37.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null )), - (0, import_react26.createElement)( -+ (0, import_react36.createElement)( ++ (0, import_react37.createElement)( Row3, { label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") }, - (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), - (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")), ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear")) ) ) ]; @@ -14093,8 +18190,8 @@ diff --git a/client.js b/client.js const toast = useUi((state) => state.toast); - const [leaving, setLeaving] = (0, import_react26.useState)(false); - (0, import_react26.useEffect)(() => { -+ const [leaving, setLeaving] = (0, import_react36.useState)(false); -+ (0, import_react36.useEffect)(() => { ++ const [leaving, setLeaving] = (0, import_react37.useState)(false); ++ (0, import_react37.useEffect)(() => { if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); @@ -14104,19 +18201,19 @@ diff --git a/client.js b/client.js if (!toast) return null; - return (0, import_react26.createElement)( + const info = toast.tone === "info"; -+ return (0, import_react36.createElement)( ++ return (0, import_react37.createElement)( "div", - { className: "bt-toast", role: "alert", "data-leaving": leaving || void 0 }, - (0, import_react26.createElement)("span", { className: "bt-toast-text" }, toast.text), - (0, import_react26.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { + { className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 }, -+ (0, import_react36.createElement)("span", { className: "bt-toast-text" }, toast.text), -+ info ? null : (0, import_react36.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { ++ (0, import_react37.createElement)("span", { className: "bt-toast-text" }, toast.text), ++ info ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => { actions.dismissToast(toast.token); actions.openSettings("feedback"); } }, t("Details")), - (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react26.createElement)(CloseIcon)) -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react36.createElement)(CloseIcon)) ++ (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react37.createElement)(CloseIcon)) ); } var FEEDBACK_CSS = ` @@ -14141,7 +18238,7 @@ diff --git a/client.js b/client.js // src/client/about.js -var import_react27 = require("react"); -var CHEER_MS = 1800; -+var import_react37 = require("react"); ++var import_react38 = require("react"); var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); function botHint(update) { - if (update.phase === "installing") return t("Updating DS Bot…"); @@ -14184,9 +18281,9 @@ diff --git a/client.js b/client.js - const [checking, setChecking] = (0, import_react27.useState)(false); - const [stars, setStars] = (0, import_react27.useState)(null); - (0, import_react27.useEffect)(() => { -+ const [checking, setChecking] = (0, import_react37.useState)(false); -+ const [stars, setStars] = (0, import_react37.useState)(null); -+ (0, import_react37.useEffect)(() => { ++ const [checking, setChecking] = (0, import_react38.useState)(false); ++ const [stars, setStars] = (0, import_react38.useState)(null); ++ (0, import_react38.useEffect)(() => { let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); @@ -14218,14 +18315,14 @@ diff --git a/client.js b/client.js const dshNew = showsDshRow(dsh, update.app); const dshShown = dshNew && dsh.available; - const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react27.createElement)( -+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react37.createElement)( ++ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react38.createElement)( "button", { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update") - ) : (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); - const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react27.createElement)( -+ ) : (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); -+ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react37.createElement)( ++ ) : (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates")); ++ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react38.createElement)( "button", { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update") @@ -14233,22 +18330,22 @@ diff --git a/client.js b/client.js const repo = update.repoUrl; return [ - (0, import_react27.createElement)( -+ (0, import_react37.createElement)( ++ (0, import_react38.createElement)( "div", { key: "hero", className: "bt-about-hero" }, - (0, import_react27.createElement)("span", { className: "bt-about-mark" }, (0, import_react27.createElement)(BotMark, { look: MAIN_LOOK, bot: { id: "about" }, size: 52, state: markState, live: true, gaze: true, pokeable: true })), - (0, import_react27.createElement)( -+ (0, import_react37.createElement)("span", { className: "bt-about-mark" }, (0, import_react37.createElement)(FlyBotMark, { size: 52 })), -+ (0, import_react37.createElement)( ++ (0, import_react38.createElement)("span", { className: "bt-about-mark" }, (0, import_react38.createElement)(FlyBotMark, { size: 52 })), ++ (0, import_react38.createElement)( "div", { className: "bt-about-copy" }, - (0, import_react27.createElement)("span", { className: "bt-about-name" }, "DS Bot"), - update.current ? (0, import_react27.createElement)("span", { className: "bt-about-version" }, t("Version {version}", { version: update.current })) : null -+ (0, import_react37.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), -+ update.current ? (0, import_react37.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null ++ (0, import_react38.createElement)("span", { className: "bt-about-name" }, t("FlyBot")), ++ update.current ? (0, import_react38.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null ), - repo ? (0, import_react27.createElement)( -+ repo ? (0, import_react37.createElement)( ++ repo ? (0, import_react38.createElement)( "a", { className: "bt-star", @@ -14262,31 +18359,31 @@ diff --git a/client.js b/client.js }, - (0, import_react27.createElement)("span", { className: "bt-star-main" }, (0, import_react27.createElement)("span", { className: "bt-star-icon" }, (0, import_react27.createElement)(StarLineIcon)), "Star"), - stars === null ? null : (0, import_react27.createElement)("span", { className: "bt-star-count" }, starCount(stars)) -+ (0, import_react37.createElement)("span", { className: "bt-star-main" }, (0, import_react37.createElement)("span", { className: "bt-star-icon" }, (0, import_react37.createElement)(StarLineIcon)), "Star"), -+ stars === null ? null : (0, import_react37.createElement)("span", { className: "bt-star-count" }, starCount(stars)) ++ (0, import_react38.createElement)("span", { className: "bt-star-main" }, (0, import_react38.createElement)("span", { className: "bt-star-icon" }, (0, import_react38.createElement)(StarLineIcon)), "Star"), ++ stars === null ? null : (0, import_react38.createElement)("span", { className: "bt-star-count" }, starCount(stars)) ) : null ), - repo ? (0, import_react27.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If DS Bot helps you, a star on GitHub helps more people find it.")) : null, - (0, import_react27.createElement)( -+ repo ? (0, import_react37.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, -+ (0, import_react37.createElement)( ++ repo ? (0, import_react38.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If FlyBot helps you, a star on GitHub helps more people find it.")) : null, ++ (0, import_react38.createElement)( Section, { key: "updates", title: t("Updates") }, - (0, import_react27.createElement)(Row3, { label: "DS Bot", hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), - dsh?.current ? (0, import_react27.createElement)(Row3, { label: "DeepSeek Harness", hint: dshHint(dsh, dshShown) }, dshControl) : null -+ (0, import_react37.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react37.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), -+ dsh?.current ? (0, import_react37.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null ++ (0, import_react38.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react38.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null), ++ dsh?.current ? (0, import_react38.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null ), - (0, import_react27.createElement)( -+ (0, import_react37.createElement)( ++ (0, import_react38.createElement)( Section, { key: "links", title: t("Links") }, - repo ? (0, import_react27.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react27.createElement)("a", linkProps(repo), (0, import_react27.createElement)(GitHubMark), t("Open"))) : null, - update.notesUrl ? (0, import_react27.createElement)(Row3, { label: t("Release notes") }, (0, import_react27.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, - (0, import_react27.createElement)(Row3, { label: t("Report a problem") }, (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) -+ repo ? (0, import_react37.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react37.createElement)("a", linkProps(repo), (0, import_react37.createElement)(GitHubMark), t("Open"))) : null, -+ update.notesUrl ? (0, import_react37.createElement)(Row3, { label: t("Release notes") }, (0, import_react37.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, -+ (0, import_react37.createElement)(Row3, { label: t("Report a problem") }, (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) ++ repo ? (0, import_react38.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react38.createElement)("a", linkProps(repo), (0, import_react38.createElement)(GitHubMark), t("Open"))) : null, ++ update.notesUrl ? (0, import_react38.createElement)(Row3, { label: t("Release notes") }, (0, import_react38.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null, ++ (0, import_react38.createElement)(Row3, { label: t("Report a problem") }, (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open"))) ) ]; } @@ -14306,24 +18403,24 @@ diff --git a/client.js b/client.js // src/client/settings.js -var import_react28 = require("react"); -+var import_react38 = require("react"); ++var import_react39 = require("react"); var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1); -var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About DS Bot"]]; +var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]]; var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon }; function SetSection({ title, children }) { - return (0, import_react28.createElement)("section", { className: "bt-set-section" }, (0, import_react28.createElement)("h3", null, title), (0, import_react28.createElement)("div", { className: "bt-set-card" }, children)); -+ return (0, import_react38.createElement)("section", { className: "bt-set-section" }, (0, import_react38.createElement)("h3", null, title), (0, import_react38.createElement)("div", { className: "bt-set-card" }, children)); ++ return (0, import_react39.createElement)("section", { className: "bt-set-section" }, (0, import_react39.createElement)("h3", null, title), (0, import_react39.createElement)("div", { className: "bt-set-card" }, children)); } function SetRow({ label, hint, children }) { - return (0, import_react28.createElement)( -+ return (0, import_react38.createElement)( ++ return (0, import_react39.createElement)( "div", { className: "bt-set-row" }, - (0, import_react28.createElement)("div", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", null, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null), - children ? (0, import_react28.createElement)("div", { className: "bt-set-control" }, children) : null -+ (0, import_react38.createElement)("div", { className: "bt-set-copy" }, (0, import_react38.createElement)("span", null, label), hint ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, hint) : null), -+ children ? (0, import_react38.createElement)("div", { className: "bt-set-control" }, children) : null ++ (0, import_react39.createElement)("div", { className: "bt-set-copy" }, (0, import_react39.createElement)("span", null, label), hint ? (0, import_react39.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ children ? (0, import_react39.createElement)("div", { className: "bt-set-control" }, children) : null ); } +var NOTIFY_KINDS = [ @@ -14338,9 +18435,9 @@ diff --git a/client.js b/client.js - const [custom, setCustom] = (0, import_react28.useState)(prefs.accent ?? "#4d6bfe"); - const timer = (0, import_react28.useRef)(0); - (0, import_react28.useEffect)(() => () => clearTimeout(timer.current), []); -+ const [custom, setCustom] = (0, import_react38.useState)(prefs.accent ?? "#4d6bfe"); -+ const timer = (0, import_react38.useRef)(0); -+ (0, import_react38.useEffect)(() => () => clearTimeout(timer.current), []); ++ const [custom, setCustom] = (0, import_react39.useState)(prefs.accent ?? "#4d6bfe"); ++ const timer = (0, import_react39.useRef)(0); ++ (0, import_react39.useEffect)(() => () => clearTimeout(timer.current), []); const pickCustom = (value) => { setCustom(value); clearTimeout(timer.current); @@ -14348,33 +18445,33 @@ diff --git a/client.js b/client.js }; return [ - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetSection, { key: "look", title: t("Appearance") }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetRow, { label: t("Theme"), hint: t("Colors for the whole team, on every device.") }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "select", { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) }, - Object.entries(themes).map(([id, theme]) => (0, import_react28.createElement)("option", { key: id, value: id }, t(theme.label))) -+ Object.entries(themes).map(([id, theme]) => (0, import_react38.createElement)("option", { key: id, value: id }, t(theme.label))) ++ Object.entries(themes).map(([id, theme]) => (0, import_react39.createElement)("option", { key: id, value: id }, t(theme.label))) ) ), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetRow, { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "div", { className: "bt-swatches" }, - (0, import_react28.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), - ACCENT_INKS.map((ink) => (0, import_react28.createElement)("button", { -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), -+ ACCENT_INKS.map((ink) => (0, import_react38.createElement)("button", { ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }), ++ ACCENT_INKS.map((ink) => (0, import_react39.createElement)("button", { key: ink, type: "button", className: "bt-swatch", @@ -14383,18 +18480,18 @@ diff --git a/client.js b/client.js onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "label", { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) }, - (0, import_react28.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) -+ (0, import_react38.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) ++ (0, import_react39.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) }) ) ) ) ), - (0, import_react28.createElement)( + // One switch for every system notification (#353), then one per kind (#469). -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetSection, - { key: "mode", title: t("Mode") }, - (0, import_react28.createElement)( @@ -14403,14 +18500,14 @@ diff --git a/client.js b/client.js - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch")) - ) + { key: "notify", title: t("Notifications") }, -+ (0, import_react38.createElement)(SwitchRow, { ++ (0, import_react39.createElement)(SwitchRow, { + label: t("Notifications"), + showLabel: false, + hint: t("When your Bots finish, fail, or wait for your approval while you are in another chat or app."), + on: prefs.notifications !== false, + onChange: (on) => run(actions.setPrefs({ notifications: on })) + }), -+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react38.createElement)(SwitchRow, { ++ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react39.createElement)(SwitchRow, { + key: kind, + label: t(label), + hint: hint ? t(hint) : void 0, @@ -14428,40 +18525,40 @@ diff --git a/client.js b/client.js - return (0, import_react28.createElement)( + const own = roster.bots.filter((bot) => !bot.remote); + const candidates = own.filter((bot) => !isMainOf(roster, bot.id)); -+ return (0, import_react38.createElement)( ++ return (0, import_react39.createElement)( SetSection, { title: t("Main Bot") }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetRow, { label: t("Main Bot"), hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0 }, - (0, import_react28.createElement)("select", { -+ (0, import_react38.createElement)("select", { ++ (0, import_react39.createElement)("select", { className: "bt-select", "aria-label": t("Main Bot"), value: first?.id ?? "", onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {})) - }, first ? null : (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), roster.bots.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ }, first ? null : (0, import_react38.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react38.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ++ }, first ? null : (0, import_react39.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react39.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ), - others.map((bot) => (0, import_react28.createElement)( -+ others.map((bot) => (0, import_react38.createElement)( ++ others.map((bot) => (0, import_react39.createElement)( SetRow, { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }, - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")), ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role")) )), - candidates.length > 0 ? (0, import_react28.createElement)( -+ candidates.length > 0 ? (0, import_react38.createElement)( ++ candidates.length > 0 ? (0, import_react39.createElement)( SetRow, { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") }, - (0, import_react28.createElement)("select", { -+ (0, import_react38.createElement)("select", { ++ (0, import_react39.createElement)("select", { className: "bt-select", "aria-label": t("Add a Main Bot"), value: "", @@ -14469,7 +18566,7 @@ diff --git a/client.js b/client.js if (event.target.value) void run(actions.setMain(event.target.value, true)); } - }, (0, import_react28.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react28.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) -+ }, (0, import_react38.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react38.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ++ }, (0, import_react39.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react39.createElement)("option", { key: bot.id, value: bot.id }, bot.name))) ) : null ); } @@ -14477,15 +18574,15 @@ diff --git a/client.js b/client.js function MotionSection({ roster, actions, run }) { const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal"; - return (0, import_react28.createElement)( -+ return (0, import_react38.createElement)( ++ return (0, import_react39.createElement)( SetSection, { title: t("Animation") }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetRow, { label: t("Bot animation") }, - (0, import_react28.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react28.createElement)("button", { -+ (0, import_react38.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react38.createElement)("button", { ++ (0, import_react39.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react39.createElement)("button", { key: id, type: "button", "aria-pressed": motion === id, @@ -14494,30 +18591,30 @@ diff --git a/client.js b/client.js } function BotsPage({ roster, actions, run }) { - const [creating, setCreating] = (0, import_react28.useState)(false); -+ const [creating, setCreating] = (0, import_react38.useState)(false); ++ const [creating, setCreating] = (0, import_react39.useState)(false); return [ - (0, import_react28.createElement)(MainBotsSection, { key: "main", roster, actions, run }), - (0, import_react28.createElement)(MotionSection, { key: "motion", roster, actions, run }), - creating ? (0, import_react28.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react28.createElement)( -+ (0, import_react38.createElement)(MainBotsSection, { key: "main", roster, actions, run }), -+ (0, import_react38.createElement)(MotionSection, { key: "motion", roster, actions, run }), -+ creating ? (0, import_react38.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react38.createElement)( ++ (0, import_react39.createElement)(MainBotsSection, { key: "main", roster, actions, run }), ++ (0, import_react39.createElement)(MotionSection, { key: "motion", roster, actions, run }), ++ creating ? (0, import_react39.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react39.createElement)( "div", { key: "new", className: "bt-actions" }, - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("New Bot")) -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react38.createElement)(PlusIcon), t("New Bot")) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react39.createElement)(PlusIcon), t("New Bot")) ), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetSection, - { key: "bots", title: t("Bots · {count}", { count: roster.bots.length }) }, - roster.bots.map((bot) => (0, import_react28.createElement)( + { key: "bots", title: t("Bots · {count}", { count: roster.bots.filter((bot) => !bot.remote).length }) }, -+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react38.createElement)( ++ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react39.createElement)( "button", { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "span", { className: "bt-set-bot" }, - (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), @@ -14525,42 +18622,42 @@ diff --git a/client.js b/client.js - bot.role ? (0, import_react28.createElement)("span", { className: "bt-tag" }, bot.role) : null, - isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, - bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ (0, import_react38.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), -+ (0, import_react38.createElement)("span", { className: "bt-row-title" }, bot.name), -+ bot.role ? (0, import_react38.createElement)("span", { className: "bt-tag" }, bot.role) : null, -+ isMainOf(roster, bot.id) ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, -+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ++ (0, import_react39.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }), ++ (0, import_react39.createElement)("span", { className: "bt-row-title" }, bot.name), ++ bot.role ? (0, import_react39.createElement)("span", { className: "bt-tag" }, bot.role) : null, ++ isMainOf(roster, bot.id) ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null, ++ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ), - (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) -+ (0, import_react38.createElement)("span", { className: "bt-chevron" }, (0, import_react38.createElement)(ChevronRightIcon)) ++ (0, import_react39.createElement)("span", { className: "bt-chevron" }, (0, import_react39.createElement)(ChevronRightIcon)) )) ) ]; } function GroupsPage({ roster, actions, run }) { - const [creating, setCreating] = (0, import_react28.useState)(false); -+ const [creating, setCreating] = (0, import_react38.useState)(false); ++ const [creating, setCreating] = (0, import_react39.useState)(false); return [ - creating ? (0, import_react28.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react28.createElement)( -+ creating ? (0, import_react38.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react38.createElement)( ++ creating ? (0, import_react39.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react39.createElement)( "div", { key: "new", className: "bt-actions" }, - (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react28.createElement)(PlusIcon), t("Create group chat")) -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react38.createElement)(PlusIcon), t("Create group chat")) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react39.createElement)(PlusIcon), t("Create group chat")) ), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( SetSection, { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) }, - roster.rooms.length === 0 ? (0, import_react28.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { -+ roster.rooms.length === 0 ? (0, import_react38.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { ++ roster.rooms.length === 0 ? (0, import_react39.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => { const admin = roster.byId[room2.admin]; - return (0, import_react28.createElement)( -+ return (0, import_react38.createElement)( ++ return (0, import_react39.createElement)( "button", { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "span", { className: "bt-set-bot" }, - (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), @@ -14568,14 +18665,14 @@ diff --git a/client.js b/client.js - (0, import_react28.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })), - admin ? (0, import_react28.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react28.createElement)(AdminStar), admin.name) : null, - room2.hidden ? (0, import_react28.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null -+ (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -+ (0, import_react38.createElement)("span", { className: "bt-row-title" }, room2.name), -+ (0, import_react38.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), -+ admin ? (0, import_react38.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react38.createElement)(AdminStar), admin.name) : null, -+ room2.hidden ? (0, import_react38.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ++ (0, import_react39.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), ++ (0, import_react39.createElement)("span", { className: "bt-row-title" }, room2.name), ++ (0, import_react39.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })), ++ admin ? (0, import_react39.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react39.createElement)(AdminStar), admin.name) : null, ++ room2.hidden ? (0, import_react39.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null ), - (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon)) -+ (0, import_react38.createElement)("span", { className: "bt-chevron" }, (0, import_react38.createElement)(ChevronRightIcon)) ++ (0, import_react39.createElement)("span", { className: "bt-chevron" }, (0, import_react39.createElement)(ChevronRightIcon)) ); }) ) @@ -14584,8 +18681,8 @@ diff --git a/client.js b/client.js function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) { - const [error, setError] = (0, import_react28.useState)(""); - const close = (0, import_react28.useCallback)(() => actions.closeSettings(), []); -+ const [error, setError] = (0, import_react38.useState)(""); -+ const close = (0, import_react38.useCallback)(() => actions.closeSettings(), []); ++ const [error, setError] = (0, import_react39.useState)(""); ++ const close = (0, import_react39.useCallback)(() => actions.closeSettings(), []); useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); @@ -14606,34 +18703,34 @@ diff --git a/client.js b/client.js const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; const depth = bot || room2 ? 1 : 0; - const last = (0, import_react28.useRef)({ key: pageKey, depth, move: "up" }); -+ const last = (0, import_react38.useRef)({ key: pageKey, depth, move: "up" }); ++ const last = (0, import_react39.useRef)({ key: pageKey, depth, move: "up" }); if (last.current.key !== pageKey) { last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" }; } - const [usageOpened, setUsageOpened] = (0, import_react28.useState)(shown === "usage"); -+ const [usageOpened, setUsageOpened] = (0, import_react38.useState)(shown === "usage"); ++ const [usageOpened, setUsageOpened] = (0, import_react39.useState)(shown === "usage"); if (shown === "usage" && !usageOpened) setUsageOpened(true); - const errorLine = error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; - return (0, import_react28.createElement)( -+ const errorLine = error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; -+ return (0, import_react38.createElement)( ++ const errorLine = error ? (0, import_react39.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; ++ return (0, import_react39.createElement)( "div", { className: "bt-settings-layer", "data-leaving": leaving || void 0 }, - (0, import_react28.createElement)("div", { className: "bt-scrim", onMouseDown: close }), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)("div", { className: "bt-scrim", onMouseDown: close }), ++ (0, import_react39.createElement)( "div", - { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("DS Bot settings"), "data-wide": shown === "usage" ? "" : void 0 }, - (0, import_react28.createElement)( + { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 }, -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "nav", { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") }, - (0, import_react28.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), - SETTINGS_PAGES.map(([key, text]) => (0, import_react28.createElement)("button", { -+ (0, import_react38.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), -+ pages.map(([key, text]) => (0, import_react38.createElement)("button", { ++ (0, import_react39.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }), ++ pages.map(([key, text]) => (0, import_react39.createElement)("button", { key, type: "button", className: "bt-settings-item", @@ -14641,10 +18738,10 @@ diff --git a/client.js b/client.js onClick: () => actions.openSettings(key) - }, (0, import_react28.createElement)(PAGE_ICONS[key]), t(text))), - (0, import_react28.createElement)("button", { -+ }, (0, import_react38.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))), ++ }, (0, import_react39.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))), + // DeepSeek Harness's own settings are a separate window with its own title and look; FlyBot + // does not embed or retitle it (never patch DSH), so the entry says it leaves (#421). -+ (0, import_react38.createElement)("button", { ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-settings-item bt-settings-shell", - title: t("Models, providers, and the rest of DeepSeek Harness"), @@ -14655,23 +18752,23 @@ diff --git a/client.js b/client.js openHarnessSettings(); } - }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings")) -+ }, (0, import_react38.createElement)(SlidersIcon), t("Harness settings"), (0, import_react38.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) ++ }, (0, import_react39.createElement)(SlidersIcon), t("Harness settings"), (0, import_react39.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗")) ), - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "div", { className: "bt-settings-main" }, - (0, import_react28.createElement)( -+ (0, import_react38.createElement)( ++ (0, import_react39.createElement)( "div", { className: "bt-settings-head" }, - back ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react28.createElement)(ChevronLeftIcon)) : null, - (0, import_react28.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) -+ back ? (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react38.createElement)(ChevronLeftIcon)) : null, -+ (0, import_react38.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) ++ back ? (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react39.createElement)(ChevronLeftIcon)) : null, ++ (0, import_react39.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label) ), - shown === "usage" ? null : (0, import_react28.createElement)( -+ shown === "usage" ? null : (0, import_react38.createElement)( ++ shown === "usage" ? null : (0, import_react39.createElement)( "div", { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move }, errorLine, @@ -14686,41 +18783,41 @@ diff --git a/client.js b/client.js - shown === "secrets" ? (0, import_react28.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, - shown === "feedback" ? (0, import_react28.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, - shown === "about" ? (0, import_react28.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null -+ shown === "general" ? (0, import_react38.createElement)(GeneralPage, { roster, actions, run }) : null, -+ bot ? (0, import_react38.createElement)("div", { className: "bt-settings-form" }, (0, import_react38.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, -+ shown === "bots" && !bot ? (0, import_react38.createElement)(BotsPage, { roster, actions, run }) : null, -+ room2 ? (0, import_react38.createElement)("div", { className: "bt-settings-form" }, (0, import_react38.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, -+ shown === "groups" && !room2 ? (0, import_react38.createElement)(GroupsPage, { roster, actions, run }) : null, -+ shown === "connectors" ? (0, import_react38.createElement)(ConnectorsPage, { roster, actions }) : null, -+ pluginPage ? (0, import_react38.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, -+ shown === "capabilities" ? (0, import_react38.createElement)(CapabilitiesPage, { roster, actions, run }) : null, -+ shown === "schedules" ? (0, import_react38.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, -+ shown === "secrets" ? (0, import_react38.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, -+ shown === "feedback" ? (0, import_react38.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, -+ shown === "about" ? (0, import_react38.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null ++ shown === "general" ? (0, import_react39.createElement)(GeneralPage, { roster, actions, run }) : null, ++ bot ? (0, import_react39.createElement)("div", { className: "bt-settings-form" }, (0, import_react39.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null, ++ shown === "bots" && !bot ? (0, import_react39.createElement)(BotsPage, { roster, actions, run }) : null, ++ room2 ? (0, import_react39.createElement)("div", { className: "bt-settings-form" }, (0, import_react39.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null, ++ shown === "groups" && !room2 ? (0, import_react39.createElement)(GroupsPage, { roster, actions, run }) : null, ++ shown === "connectors" ? (0, import_react39.createElement)(ConnectorsPage, { roster, actions }) : null, ++ pluginPage ? (0, import_react39.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null, ++ shown === "capabilities" ? (0, import_react39.createElement)(CapabilitiesPage, { roster, actions, run }) : null, ++ shown === "schedules" ? (0, import_react39.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null, ++ shown === "secrets" ? (0, import_react39.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null, ++ shown === "feedback" ? (0, import_react39.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null, ++ shown === "about" ? (0, import_react39.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null ), - usageOpened ? (0, import_react28.createElement)( -+ usageOpened ? (0, import_react38.createElement)( ++ usageOpened ? (0, import_react39.createElement)( "div", { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true }, shown === "usage" ? errorLine : null, - (0, import_react28.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) -+ (0, import_react38.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) ++ (0, import_react39.createElement)(UsagePage, { roster, actions, focusId: usageFocus }) ) : null ), - (0, import_react28.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react28.createElement)(CloseIcon)) -+ (0, import_react38.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react38.createElement)(CloseIcon)) ++ (0, import_react39.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react39.createElement)(CloseIcon)) ) ); } // src/client/context-menu.js -var import_react29 = require("react"); -+var import_react39 = require("react"); ++var import_react40 = require("react"); function ContextMenu({ menu, roster, actions, leaving = false }) { useEscape(() => actions.closeMenu()); - (0, import_react29.useEffect)(() => { -+ (0, import_react39.useEffect)(() => { ++ (0, import_react40.useEffect)(() => { const onDown = (event) => { if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu(); }; @@ -14731,15 +18828,15 @@ diff --git a/client.js b/client.js - const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); - (0, import_react29.useLayoutEffect)(() => { + const structure = useSidebarStructure(); -+ const ref = (0, import_react39.useRef)(null); -+ const [place, setPlace] = (0, import_react39.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react39.useLayoutEffect)(() => { ++ const ref = (0, import_react40.useRef)(null); ++ const [place, setPlace] = (0, import_react40.useState)({ left: menu.x, top: menu.y }); ++ (0, import_react40.useLayoutEffect)(() => { const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); }, [menu.x, menu.y]); - if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react29.createElement)(AgentMenu, { menu, roster, actions, leaving }); -+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react39.createElement)(AgentMenu, { menu, roster, actions, leaving }); ++ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react40.createElement)(AgentMenu, { menu, roster, actions, leaving }); const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; @@ -14782,18 +18879,18 @@ diff --git a/client.js b/client.js - return (0, import_react29.createElement)( + ]; + const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0); -+ return (0, import_react39.createElement)( ++ return (0, import_react40.createElement)( "div", { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 }, - groups.map((group, index) => (0, import_react29.createElement)( - import_react29.Fragment, -+ shown.map((group, index) => (0, import_react39.createElement)( -+ import_react39.Fragment, ++ shown.map((group, index) => (0, import_react40.createElement)( ++ import_react40.Fragment, { key: index }, - index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { -+ index > 0 ? (0, import_react39.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react39.createElement)("button", { ++ index > 0 ? (0, import_react40.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, danger]) => (0, import_react40.createElement)("button", { key: label, type: "button", role: "menuitem", @@ -14802,7 +18899,7 @@ diff --git a/client.js b/client.js void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } - }, (0, import_react29.createElement)(Icon), label)) -+ }, (0, import_react39.createElement)(Icon), label)) ++ }, (0, import_react40.createElement)(Icon), label)) )) ); } @@ -14810,9 +18907,9 @@ diff --git a/client.js b/client.js - const ref = (0, import_react29.useRef)(null); - const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y }); - (0, import_react29.useLayoutEffect)(() => { -+ const ref = (0, import_react39.useRef)(null); -+ const [place, setPlace] = (0, import_react39.useState)({ left: menu.x, top: menu.y }); -+ (0, import_react39.useLayoutEffect)(() => { ++ const ref = (0, import_react40.useRef)(null); ++ const [place, setPlace] = (0, import_react40.useState)({ left: menu.x, top: menu.y }); ++ (0, import_react40.useLayoutEffect)(() => { const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); @@ -14821,27 +18918,27 @@ diff --git a/client.js b/client.js ] ]; - return (0, import_react29.createElement)( -+ return (0, import_react39.createElement)( ++ return (0, import_react40.createElement)( "div", { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 }, - groups.map((group, index) => (0, import_react29.createElement)( - import_react29.Fragment, -+ groups.map((group, index) => (0, import_react39.createElement)( -+ import_react39.Fragment, ++ groups.map((group, index) => (0, import_react40.createElement)( ++ import_react40.Fragment, { key: index }, - index > 0 ? (0, import_react29.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, - group.map(([label, Icon, run, danger]) => (0, import_react29.createElement)("button", { -+ index > 0 ? (0, import_react39.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -+ group.map(([label, Icon, run, danger]) => (0, import_react39.createElement)("button", { ++ index > 0 ? (0, import_react40.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, ++ group.map(([label, Icon, run, danger]) => (0, import_react40.createElement)("button", { key: label, type: "button", role: "menuitem", -@@ -10968,16 +10968,31 @@ function AgentMenu({ menu, roster, actions, leaving }) { +@@ -10968,303 +10968,93 @@ function AgentMenu({ menu, roster, actions, leaving }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } - }, (0, import_react29.createElement)(Icon), label)) -+ }, (0, import_react39.createElement)(Icon), label)) ++ }, (0, import_react40.createElement)(Icon), label)) )) ); } @@ -14864,15 +18961,67 @@ diff --git a/client.js b/client.js var STORAGE = { unread: "ds-bot:unread", reactions: "ds-bot:reactions", - seen: "ds-bot:seen", -+ friendLines: "ds-bot:friend-lines", - prefs: "ds-bot:prefs", - surface: "ds-bot:surface", - agentSessions: "ds-bot:agent-sessions", -@@ -11038,233 +11038,8 @@ function migrateStorage(storage = globalThis.localStorage) { - } - } - +- seen: "ds-bot:seen", +- prefs: "ds-bot:prefs", +- surface: "ds-bot:surface", +- agentSessions: "ds-bot:agent-sessions", +- browserWidth: "ds-bot:browser-width", +- browsers: "ds-bot.browser.v2" +-}; +-var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"]; +-var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"]; +-var LEGACY_THEMES = { grok: "mono" }; +-function legacyKeys(key) { +- if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS; +- const name = key.slice(key.indexOf(":") + 1); +- return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`); +-} +-var migrateBrowsers = (storage) => { +- if (storage.getItem(STORAGE.browsers) !== null) { +- for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key); +- return; +- } +- for (const key of LEGACY_BROWSER_KEYS) { +- try { +- const value = storage.getItem(key); +- if (value === null) continue; +- const previous = JSON.parse(value) ?? {}; +- const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => { +- const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" }; +- return [botId, { tabs: [tab], active: tab.id }]; +- })); +- storage.setItem(STORAGE.browsers, JSON.stringify(tabs)); +- for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier); +- return; +- } catch { +- } +- } +-}; +-function migrateStorage(storage = globalThis.localStorage) { +- for (const key of Object.values(STORAGE)) { +- if (key === STORAGE.browsers) continue; +- for (const legacy of legacyKeys(key)) { +- try { +- const value = storage.getItem(legacy); +- if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); +- storage.removeItem(legacy); +- } catch { +- } +- } +- } +- try { +- migrateBrowsers(storage); +- } catch { +- } +- try { +- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); +- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { +- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); +- } +- } catch { +- } +-} +- -// src/client/member-status.js -var import_react30 = require("react"); -var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -15055,13 +19204,72 @@ diff --git a/client.js b/client.js - entry.here ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null - ) - ); --} ++ seen: "ds-bot:seen", ++ friendLines: "ds-bot:friend-lines", ++ prefs: "ds-bot:prefs", ++ surface: "ds-bot:surface", ++ agentSessions: "ds-bot:agent-sessions", ++ browserWidth: "ds-bot:browser-width", ++ browsers: "ds-bot.browser.v2" ++}; ++var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"]; ++var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"]; ++var LEGACY_THEMES = { grok: "mono" }; ++function legacyKeys(key) { ++ if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS; ++ const name = key.slice(key.indexOf(":") + 1); ++ return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`); + } -function useMemberWork(room2, useWork) { - const work = useWork?.((value) => value) ?? NO_WORK; - const entries2 = room2 ? memberWork(room2, work) : []; - const now = useNow2(entries2.some((entry) => entry.state !== "idle")); - return { entries: entries2, now }; --} ++var migrateBrowsers = (storage) => { ++ if (storage.getItem(STORAGE.browsers) !== null) { ++ for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key); ++ return; ++ } ++ for (const key of LEGACY_BROWSER_KEYS) { ++ try { ++ const value = storage.getItem(key); ++ if (value === null) continue; ++ const previous = JSON.parse(value) ?? {}; ++ const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => { ++ const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" }; ++ return [botId, { tabs: [tab], active: tab.id }]; ++ })); ++ storage.setItem(STORAGE.browsers, JSON.stringify(tabs)); ++ for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier); ++ return; ++ } catch { ++ } ++ } ++}; ++function migrateStorage(storage = globalThis.localStorage) { ++ for (const key of Object.values(STORAGE)) { ++ if (key === STORAGE.browsers) continue; ++ for (const legacy of legacyKeys(key)) { ++ try { ++ const value = storage.getItem(legacy); ++ if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); ++ storage.removeItem(legacy); ++ } catch { ++ } ++ } ++ } ++ try { ++ migrateBrowsers(storage); ++ } catch { ++ } ++ try { ++ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); ++ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { ++ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); ++ } ++ } catch { ++ } + } -var MEMBER_CSS = ` -.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} -.bt-mw-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} @@ -15097,10 +19305,10 @@ diff --git a/client.js b/client.js -@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} -@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} -`; -- + // src/client/details-drawer.js -var import_react31 = require("react"); -+var import_react40 = require("react"); ++var import_react41 = require("react"); function transcriptOf(list, id, name, hiddenTurns = []) { const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; if (!Array.isArray(outline)) return ""; @@ -15109,11 +19317,11 @@ diff --git a/client.js b/client.js var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { - const [pending, setPending] = (0, import_react31.useState)(null); -+ const [pending, setPending] = (0, import_react40.useState)(null); ++ const [pending, setPending] = (0, import_react41.useState)(null); const catalog = roster.permissions; const saved = bot.permission ?? catalog?.defaultPreset; - (0, import_react31.useEffect)(() => { -+ (0, import_react40.useEffect)(() => { ++ (0, import_react41.useEffect)(() => { if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; @@ -15122,17 +19330,17 @@ diff --git a/client.js b/client.js void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; - return (0, import_react31.createElement)( -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( "div", { className: "bt-drawer-permission" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Permissions")), -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Permissions")), ++ (0, import_react41.createElement)( Segmented, { label: t("Permissions"), value: current }, - catalog.options.map((option) => (0, import_react31.createElement)("button", { -+ catalog.options.map((option) => (0, import_react40.createElement)("button", { ++ catalog.options.map((option) => (0, import_react41.createElement)("button", { key: option.value, type: "button", "aria-pressed": option.value === current, @@ -15141,56 +19349,56 @@ diff --git a/client.js b/client.js const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); - (0, import_react31.useEffect)(() => { -+ (0, import_react40.useEffect)(() => { ++ (0, import_react41.useEffect)(() => { actions.browserEnsure?.(bot.id); }, [bot.id]); if (roster.browser !== true || !actions.browserShow) { - return (0, import_react31.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); -+ return (0, import_react40.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); ++ return (0, import_react41.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app.")); } if (state.ids.length === 0) { - return (0, import_react31.createElement)( -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( "div", { className: "bt-browser-empty-mark" }, - (0, import_react31.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), - (0, import_react31.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), - (0, import_react31.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), - (0, import_react31.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) -+ (0, import_react40.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), -+ (0, import_react40.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react40.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) ++ (0, import_react41.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }), ++ (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), ++ (0, import_react41.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })), ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser")) ); } - return (0, import_react31.createElement)( -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( "div", { className: "bt-bcards" }, state.ids.map((tabId, index) => { const page = pages[tabId] ?? {}; const label = page.title || shortUrl(page.url) || t("New tab"); - return (0, import_react31.createElement)( -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( "div", { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } }, - (0, import_react31.createElement)( -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "button", { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) }, - page.excerpt ? (0, import_react31.createElement)( -+ page.excerpt ? (0, import_react40.createElement)( ++ page.excerpt ? (0, import_react41.createElement)( "span", { className: "bt-bcard-preview" }, - (0, import_react31.createElement)("span", { className: "bt-bcard-ptitle" }, label), - (0, import_react31.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) - ) : (0, import_react31.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react31.createElement)(GlobeIcon), (0, import_react31.createElement)("span", null, label)) -+ (0, import_react40.createElement)("span", { className: "bt-bcard-ptitle" }, label), -+ (0, import_react40.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) -+ ) : (0, import_react40.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react40.createElement)(GlobeIcon), (0, import_react40.createElement)("span", null, label)) ++ (0, import_react41.createElement)("span", { className: "bt-bcard-ptitle" }, label), ++ (0, import_react41.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt) ++ ) : (0, import_react41.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react41.createElement)(GlobeIcon), (0, import_react41.createElement)("span", null, label)) ), - (0, import_react31.createElement)("button", { -+ (0, import_react40.createElement)("button", { ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), @@ -15199,46 +19407,43 @@ diff --git a/client.js b/client.js } }, "×"), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { className: "bt-bcard-row" }, - (0, import_react31.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), - (0, import_react31.createElement)("span", { className: "bt-bcard-title" }, label), - page.url ? (0, import_react31.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null -+ (0, import_react40.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), -+ (0, import_react40.createElement)("span", { className: "bt-bcard-title" }, label), -+ page.url ? (0, import_react40.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null ++ (0, import_react41.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }), ++ (0, import_react41.createElement)("span", { className: "bt-bcard-title" }, label), ++ page.url ? (0, import_react41.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null ) ); }), - (0, import_react31.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react31.createElement)(PlusIcon), t("New tab")) -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react40.createElement)(PlusIcon), t("New tab")) ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react41.createElement)(PlusIcon), t("New tab")) ); } function About({ text, title = "About" }) { - const [open, setOpen] = (0, import_react31.useState)(false); -+ const [open, setOpen] = (0, import_react40.useState)(false); ++ const [open, setOpen] = (0, import_react41.useState)(false); const long = text.split("\n").length > 5 || text.length > 240; - const body = (0, import_react31.useRef)(null); -+ const body = (0, import_react40.useRef)(null); ++ const body = (0, import_react41.useRef)(null); useMorph(body, open); - return (0, import_react31.createElement)( -+ return (0, import_react40.createElement)( - "div", - null, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t(title)), -+ (0, import_react40.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -+ long ? (0, import_react40.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ return (0, import_react41.createElement)( ++ "div", ++ null, ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t(title)), ++ (0, import_react41.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), ++ long ? (0, import_react41.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null + ); +} +function SharedRoomApprovals({ room: room2 }) { -+ const [items, setItems] = (0, import_react40.useState)(room2.pendingApprovals ?? []); -+ const [busy, setBusy] = (0, import_react40.useState)(""); ++ const [items, setItems] = (0, import_react41.useState)(room2.pendingApprovals ?? []); ++ const [busy, setBusy] = (0, import_react41.useState)(""); + const face = room2.origin === "face-to-face"; -+ (0, import_react40.useEffect)(() => { ++ (0, import_react41.useEffect)(() => { + const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length; + if (haveItems) setItems(room2.pendingApprovals); + if (room2.viewerRole !== "invited-owner" && !face) return void 0; @@ -15264,28 +19469,31 @@ diff --git a/client.js b/client.js + setBusy(""); + } + }; -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( + "div", + { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" }, -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react40.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), -+ items.map((item) => (0, import_react40.createElement)( ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react41.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))), ++ items.map((item) => (0, import_react41.createElement)( + "div", + { key: item.id, className: "bt-set-row" }, -+ (0, import_react40.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) ++ (0, import_react41.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")), ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")), ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject")) + )) + ); +} +function FaceMembers({ room: room2, roster, actions }) { + const people2 = facePeople(room2, roster); + const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); -+ return (0, import_react40.createElement)( -+ "div", -+ null, -+ (0, import_react40.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), -+ (0, import_react40.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react40.createElement)(FacePersonRow, { ++ return (0, import_react41.createElement)( + "div", + null, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), +- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), +- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ (0, import_react41.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), ++ (0, import_react41.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react41.createElement)(FacePersonRow, { + key: person.key, + person, + roster, @@ -15311,31 +19519,31 @@ diff --git a/client.js b/client.js return [ - room2.notice ? (0, import_react31.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react31.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), -+ invited && room2.sharedFrom && !face ? (0, import_react40.createElement)( ++ (0, import_react41.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }), ++ invited && room2.sharedFrom && !face ? (0, import_react41.createElement)( + "div", + { key: "from", className: "bt-shared-from" }, -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, title), -+ (0, import_react40.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), -+ room2.sharedFrom.invitedAt ? (0, import_react40.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) -+ ) : face ? null : room2.notice ? (0, import_react40.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react40.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), -+ face ? null : (0, import_react40.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), -+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react40.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react40.createElement)( ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, title), ++ (0, import_react41.createElement)("div", { className: "bt-set-hint" }, t("Your Bot was invited to this room by {name}", { name: room2.sharedFrom.inviterName })), ++ room2.sharedFrom.invitedAt ? (0, import_react41.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null, ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot")) ++ ) : face ? null : room2.notice ? (0, import_react41.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react41.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")), ++ face ? null : (0, import_react41.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }), ++ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react41.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react41.createElement)( "div", { key: "members" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })), - (0, import_react31.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => { - const entry = entries2.find((item) => item.id === member.id); - return (0, import_react31.createElement)( -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), -+ (0, import_react40.createElement)("div", { className: "bt-members", role: "list" }, [ -+ ...humans.map((person) => (0, import_react40.createElement)( ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })), ++ (0, import_react41.createElement)("div", { className: "bt-members", role: "list" }, [ ++ ...humans.map((person) => (0, import_react41.createElement)( "div", - { key: member.id, role: "listitem" }, - (0, import_react31.createElement)( + { key: person.id, role: "listitem" }, -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { className: "bt-member" }, - (0, import_react31.createElement)( @@ -15343,70 +19551,70 @@ diff --git a/client.js b/client.js - { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, - (0, import_react31.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), - (0, import_react31.createElement)("span", null, member.name) -+ (0, import_react40.createElement)("span", { className: "bt-member-name" }, (0, import_react40.createElement)("span", null, person.displayName || person.id)), -+ (0, import_react40.createElement)("span", { className: "bt-chip" }, t("Human")), -+ person.role === "admin" ? (0, import_react40.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ++ (0, import_react41.createElement)("span", { className: "bt-member-name" }, (0, import_react41.createElement)("span", null, person.displayName || person.id)), ++ (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Human")), ++ person.role === "admin" ? (0, import_react41.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null + ) + )), -+ ...remotes.map((person) => (0, import_react40.createElement)( ++ ...remotes.map((person) => (0, import_react41.createElement)( + "div", + { key: person.handle || person.id, role: "listitem" }, -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( + "div", + { className: "bt-member" }, -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( + "span", + { className: "bt-member-name" }, -+ (0, import_react40.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), -+ (0, import_react40.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) -+ ), -+ face ? null : (0, import_react40.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) ++ (0, import_react41.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }), ++ (0, import_react41.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person)) + ), +- room2.admin === member.id ? (0, import_react31.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null +- ), +- entry ? (0, import_react31.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null +- ); +- })) ++ face ? null : (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) + ) + )), + ...members.map((member) => { + const entry = entries2.find((item) => item.id === member.id); -+ return (0, import_react40.createElement)( ++ return (0, import_react41.createElement)( + "div", + { key: member.id, role: "listitem" }, -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( + "div", + { className: "bt-member" }, -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( + "button", + { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) }, -+ (0, import_react40.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), -+ (0, import_react40.createElement)("span", null, member.name) ++ (0, import_react41.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }), ++ (0, import_react41.createElement)("span", null, member.name) + ), -+ room2.admin === member.id ? (0, import_react40.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null - ), -- room2.admin === member.id ? (0, import_react31.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null -- ), -- entry ? (0, import_react31.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null -- ); -- })) -+ entry ? (0, import_react40.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null ++ room2.admin === member.id ? (0, import_react41.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null ++ ), ++ entry ? (0, import_react41.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null + ); + }) + ]) ), - (0, import_react31.createElement)( -+ face ? null : (0, import_react40.createElement)( ++ face ? null : (0, import_react41.createElement)( "div", { key: "mode" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Who answers")), - (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Who answers")), -+ (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Who answers")), ++ (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone"))) ), - usage ? (0, import_react31.createElement)( -+ face || !usage ? null : (0, import_react40.createElement)( ++ face || !usage ? null : (0, import_react41.createElement)( "div", { key: "usage" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), - (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, room2.name), (0, import_react31.createElement)("span", null, usage)) - ) : null -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, room2.name), (0, import_react40.createElement)("span", null, usage)) ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Usage")), ++ (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, room2.name), (0, import_react41.createElement)("span", null, usage)) + ) ]; } @@ -15415,21 +19623,21 @@ diff --git a/client.js b/client.js const part = owner ? currentPart(owner) : sessionId; const isMainBot = isMainOf(roster, owner?.id); - const [tab, setTab] = (0, import_react31.useState)("details"); -+ const [tab, setTab] = (0, import_react40.useState)("details"); ++ const [tab, setTab] = (0, import_react41.useState)("details"); const room2 = roster.roomsById[sessionId]; - const [view, setView] = (0, import_react31.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); - const [editing, setEditing] = (0, import_react31.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); - const [draftName, setDraftName] = (0, import_react31.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); - const [copied, setCopied] = (0, import_react31.useState)(false); - const [error, setError] = (0, import_react31.useState)(""); -+ const [view, setView] = (0, import_react40.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); -+ const [editing, setEditing] = (0, import_react40.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); -+ const [draftName, setDraftName] = (0, import_react40.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); -+ const [copied, setCopied] = (0, import_react40.useState)(false); -+ const [error, setError] = (0, import_react40.useState)(""); ++ const [view, setView] = (0, import_react41.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview"); ++ const [editing, setEditing] = (0, import_react41.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId]))); ++ const [draftName, setDraftName] = (0, import_react41.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : "")); ++ const [copied, setCopied] = (0, import_react41.useState)(false); ++ const [error, setError] = (0, import_react41.useState)(""); const glide = useGlide(tab); - const last = (0, import_react31.useRef)({ view, tab, move: void 0 }); -+ const last = (0, import_react40.useRef)({ view, tab, move: void 0 }); ++ const last = (0, import_react41.useRef)({ view, tab, move: void 0 }); if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; @@ -15439,56 +19647,56 @@ diff --git a/client.js b/client.js }; - const errorLine = error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; - if ((bot || room2) && view === "settings") { -+ const errorLine = error ? (0, import_react40.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; ++ const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null; + if ((bot || room2) && view === "settings" && !bot?.remote) { return [ - (0, import_react31.createElement)( -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { key: "head", className: "bt-subhead", "data-move": move }, - (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react31.createElement)(ChevronLeftIcon)), - (0, import_react31.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), - hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) -+ (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react40.createElement)(ChevronLeftIcon)), -+ (0, import_react40.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), -+ hasStrip ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react40.createElement)(CloseIcon)) ++ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react41.createElement)(ChevronLeftIcon)), ++ (0, import_react41.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")), ++ hasStrip ? null : (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) ), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move }, errorLine, - bot ? (0, import_react31.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react31.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) -+ bot ? (0, import_react40.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react40.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) ++ bot ? (0, import_react41.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react41.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close }) ) ]; } - const top = (0, import_react31.createElement)( -+ const top = (0, import_react40.createElement)( ++ const top = (0, import_react41.createElement)( "div", { className: "bt-drawer-top", "data-move": side }, - bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? t("Edit Bot") : t("Group settings"), title: bot ? t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => setView("settings") }, (0, import_react31.createElement)(GearIcon)) : null, - bot && roster.browser === true && actions.openBrowser ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react31.createElement)(GlobeIcon)) : null, - bot || room2 ? (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react31.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react31.createElement)(ShareIcon)) : null, - hasStrip ? null : (0, import_react31.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react31.createElement)(CloseIcon)) -+ bot || room2 ? (0, import_react40.createElement)("button", { ++ bot || room2 ? (0, import_react41.createElement)("button", { + type: "button", + className: "bt-round", + "aria-label": bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), + title: bot ? bot.remote ? t("Bot settings") : t("Edit Bot") : t("Group settings"), + "data-info-row": "settings", + onClick: () => bot?.remote ? actions.openSettings("bots", bot.id) : setView("settings") -+ }, (0, import_react40.createElement)(GearIcon)) : null, -+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react40.createElement)(GlobeIcon)) : null, -+ bot || room2 ? (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react40.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react40.createElement)(ShareIcon)) : null, -+ hasStrip ? null : (0, import_react40.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react40.createElement)(CloseIcon)) ++ }, (0, import_react41.createElement)(GearIcon)) : null, ++ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react41.createElement)(GlobeIcon)) : null, ++ bot || room2 ? (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react41.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react41.createElement)(ShareIcon)) : null, ++ hasStrip ? null : (0, import_react41.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react41.createElement)(CloseIcon)) ); if (!bot && !room2) { return [ - (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), - (0, import_react31.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react31.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) -+ (0, import_react40.createElement)(import_react40.Fragment, { key: "top" }, top), -+ (0, import_react40.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react40.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) ++ (0, import_react41.createElement)(import_react41.Fragment, { key: "top" }, top), ++ (0, import_react41.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react41.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team."))) ]; } const current = bot?.name ?? room2.name; @@ -15498,14 +19706,14 @@ diff --git a/client.js b/client.js return [ - (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)(import_react40.Fragment, { key: "top" }, top), -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)(import_react41.Fragment, { key: "top" }, top), ++ (0, import_react41.createElement)( "div", { key: "id", className: "bt-drawer-id", "data-move": side }, - bot ? (0, import_react31.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), - editing ? (0, import_react31.createElement)("input", { -+ bot ? (0, import_react40.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react40.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), -+ bot?.remote ? (0, import_react40.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react40.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react40.createElement)("input", { ++ bot ? (0, import_react41.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), ++ bot?.remote ? (0, import_react41.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react41.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react41.createElement)("input", { className: "bt-drawer-name-input", autoFocus: true, value: draftName, @@ -15514,82 +19722,82 @@ diff --git a/client.js b/client.js } } - }) : (0, import_react31.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { -+ }) : (0, import_react40.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { ++ }) : (0, import_react41.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => { setDraftName(room2 && !room2.named ? "" : current); setEditing(true); } }, current), - subtitle ? (0, import_react31.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null -+ subtitle ? (0, import_react40.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react40.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null ++ subtitle ? (0, import_react41.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react41.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null ), - (0, import_react31.createElement)( -+ bot?.remote ? null : (0, import_react40.createElement)( ++ bot?.remote ? null : (0, import_react41.createElement)( "div", { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side }, - (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - tabs.map((key) => (0, import_react31.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) -+ (0, import_react40.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ tabs.map((key) => (0, import_react40.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) ++ (0, import_react41.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ tabs.map((key) => (0, import_react41.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key]))) ), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move }, errorLine, - room2 && tab === "details" ? (0, import_react31.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, - bot && tab === "details" ? [ - (0, import_react31.createElement)( -+ room2 && tab === "details" ? (0, import_react40.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, ++ room2 && tab === "details" ? (0, import_react41.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null, + bot && bot.remote && tab === "details" ? [ -+ bot.remote.handle ? (0, import_react40.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, -+ (0, import_react40.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) ++ bot.remote.handle ? (0, import_react41.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null, ++ (0, import_react41.createElement)(FlyBotDetails, { key: "social", bot, roster, actions }) + ] : null, + bot && !bot.remote && tab === "details" ? [ -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)( "div", { key: "model", className: "bt-drawer-model" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Model")), - (0, import_react31.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), - modelOf(bot, roster) ? null : (0, import_react31.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Model")), -+ (0, import_react40.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), -+ modelOf(bot, roster) ? null : (0, import_react40.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Model")), ++ (0, import_react41.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }), ++ modelOf(bot, roster) ? null : (0, import_react41.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name })) ), - (0, import_react31.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), - (0, import_react31.createElement)( -+ (0, import_react40.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), -+ (0, import_react40.createElement)( ++ (0, import_react41.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }), ++ (0, import_react41.createElement)( "div", { key: "folder", className: "bt-drawer-model" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Working folder")), - (0, import_react31.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Working folder")), -+ (0, import_react40.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Working folder")), ++ (0, import_react41.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) }) ), - bot.instructions ? (0, import_react31.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react31.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), - roster.memory === false ? null : (0, import_react31.createElement)(MemoryRow, { key: "memory", bot, actions }), - usage ? (0, import_react31.createElement)( -+ bot.instructions ? (0, import_react40.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react40.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), -+ roster.memory === false ? null : (0, import_react40.createElement)(MemoryRow, { key: "memory", bot, actions }), -+ usage ? (0, import_react40.createElement)( ++ bot.instructions ? (0, import_react41.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react41.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")), ++ roster.memory === false ? null : (0, import_react41.createElement)(MemoryRow, { key: "memory", bot, actions }), ++ usage ? (0, import_react41.createElement)( "div", { key: "usage" }, - (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Usage")), - (0, import_react31.createElement)("div", { className: "bt-kv" }, (0, import_react31.createElement)("span", null, bot.name), (0, import_react31.createElement)("span", null, usage)) -+ (0, import_react40.createElement)("div", { className: "bt-section-title" }, t("Usage")), -+ (0, import_react40.createElement)("div", { className: "bt-kv" }, (0, import_react40.createElement)("span", null, bot.name), (0, import_react40.createElement)("span", null, usage)) ++ (0, import_react41.createElement)("div", { className: "bt-section-title" }, t("Usage")), ++ (0, import_react41.createElement)("div", { className: "bt-kv" }, (0, import_react41.createElement)("span", null, bot.name), (0, import_react41.createElement)("span", null, usage)) ) : null, - (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions }) -+ (0, import_react40.createElement)(ScheduleSummary, { key: "routines", bot, actions }) ++ (0, import_react41.createElement)(ScheduleSummary, { key: "routines", bot, actions }) ] : null, - bot && tab === "browser" ? (0, import_react31.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null -+ bot && !bot.remote && tab === "browser" ? (0, import_react40.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null ++ bot && !bot.remote && tab === "browser" ? (0, import_react41.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null ) ]; } // src/client/browser-pane.js -var import_react32 = require("react"); -+var import_react41 = require("react"); ++var import_react42 = require("react"); var import_react_dom8 = require("react-dom"); var PANEL_LINGER_MS = 300; var WIDTH_KEY = STORAGE.browserWidth; @@ -15606,11 +19814,11 @@ diff --git a/client.js b/client.js -var icon = (paths) => function BrowserIcon() { - return (0, import_react32.createElement)( +var icon2 = (paths) => function BrowserIcon() { -+ return (0, import_react41.createElement)( ++ return (0, import_react42.createElement)( "svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, - paths.map((d, index) => (0, import_react32.createElement)("path", { key: index, d })) -+ paths.map((d, index) => (0, import_react41.createElement)("path", { key: index, d })) ++ paths.map((d, index) => (0, import_react42.createElement)("path", { key: index, d })) ); }; -var GlobeIcon = icon(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); @@ -15633,9 +19841,9 @@ diff --git a/client.js b/client.js - const [failed, setFailed] = (0, import_react32.useState)(null); - if (!src || failed === src) return (0, import_react32.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react32.createElement)(GlobeIcon)); - return (0, import_react32.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); -+ const [failed, setFailed] = (0, import_react41.useState)(null); -+ if (!src || failed === src) return (0, import_react41.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react41.createElement)(GlobeIcon)); -+ return (0, import_react41.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); ++ const [failed, setFailed] = (0, import_react42.useState)(null); ++ if (!src || failed === src) return (0, import_react42.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react42.createElement)(GlobeIcon)); ++ return (0, import_react42.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) }); } var savedWidth = () => { try { @@ -15658,11 +19866,11 @@ diff --git a/client.js b/client.js - const spot = (0, import_react32.useRef)(null); - const under = (0, import_react32.useRef)(null); - (0, import_react32.useLayoutEffect)(() => { -+ const wrap = (0, import_react41.useRef)(null); -+ const ind = (0, import_react41.useRef)(null); -+ const spot = (0, import_react41.useRef)(null); -+ const under = (0, import_react41.useRef)(null); -+ (0, import_react41.useLayoutEffect)(() => { ++ const wrap = (0, import_react42.useRef)(null); ++ const ind = (0, import_react42.useRef)(null); ++ const spot = (0, import_react42.useRef)(null); ++ const under = (0, import_react42.useRef)(null); ++ (0, import_react42.useLayoutEffect)(() => { const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; @@ -15671,7 +19879,7 @@ diff --git a/client.js b/client.js }; }); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { @@ -15680,15 +19888,15 @@ diff --git a/client.js b/client.js return () => observer.disconnect(); }, []); - return (0, import_react32.createElement)( -+ return (0, import_react41.createElement)( ++ return (0, import_react42.createElement)( "div", { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") }, - (0, import_react32.createElement)( -+ (0, import_react41.createElement)( ++ (0, import_react42.createElement)( "div", { className: "bt-ptabs-in", ref: wrap }, - (0, import_react32.createElement)( -+ (0, import_react41.createElement)( ++ (0, import_react42.createElement)( "button", { type: "button", @@ -15698,14 +19906,14 @@ diff --git a/client.js b/client.js }, - (0, import_react32.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), - view === "details" ? (0, import_react32.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null -+ (0, import_react41.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), -+ view === "details" ? (0, import_react41.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null ++ (0, import_react42.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }), ++ view === "details" ? (0, import_react42.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null ), ids.map((tabId) => { const page = pages[tabId] ?? EMPTY_PAGE; const on = view === "browser" && tabId === active; - return (0, import_react32.createElement)( -+ return (0, import_react41.createElement)( ++ return (0, import_react42.createElement)( "button", { key: tabId, @@ -15716,9 +19924,9 @@ diff --git a/client.js b/client.js - (0, import_react32.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), - (0, import_react32.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), - (0, import_react32.createElement)("span", { -+ (0, import_react41.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), -+ (0, import_react41.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), -+ (0, import_react41.createElement)("span", { ++ (0, import_react42.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }), ++ (0, import_react42.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)), ++ (0, import_react42.createElement)("span", { className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), @@ -15727,13 +19935,13 @@ diff --git a/client.js b/client.js ); }), - (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) -+ (0, import_react41.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) ++ (0, import_react42.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true }) ), // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both. - (0, import_react32.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react32.createElement)(PlusTabIcon)), - (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react32.createElement)(CloseIcon2)) -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react41.createElement)(PlusTabIcon)), -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react41.createElement)(CloseIcon2)) ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react42.createElement)(PlusTabIcon)), ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react42.createElement)(CloseIcon2)) ); } function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) { @@ -15741,12 +19949,12 @@ diff --git a/client.js b/client.js - const frame = (0, import_react32.useRef)(null); - const input = (0, import_react32.useRef)(null); - const [draft, setDraft] = (0, import_react32.useState)(null); -+ const frame = (0, import_react41.useRef)(null); -+ const input = (0, import_react41.useRef)(null); -+ const [draft, setDraft] = (0, import_react41.useState)(null); ++ const frame = (0, import_react42.useRef)(null); ++ const input = (0, import_react42.useRef)(null); ++ const [draft, setDraft] = (0, import_react42.useState)(null); const showGuest = landed && !leaving && Boolean(page.url) && !page.error; - (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react41.useLayoutEffect)(() => { ++ (0, import_react42.useLayoutEffect)(() => { const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); @@ -15755,7 +19963,7 @@ diff --git a/client.js b/client.js }; }, [tabId, showGuest, leaving]); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { setDraft(null); if (landed && !page.url) input.current?.focus(); }, [tabId, landed]); @@ -15764,19 +19972,19 @@ diff --git a/client.js b/client.js const editing = draft !== null; const secure = /^https:/i.test(page.url); - return (0, import_react32.createElement)( -+ return (0, import_react41.createElement)( ++ return (0, import_react42.createElement)( "div", { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) }, - (0, import_react32.createElement)( -+ (0, import_react41.createElement)( ++ (0, import_react42.createElement)( "div", { className: "bt-browser-bar" }, - (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react32.createElement)(BackIcon)), - (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react32.createElement)(ForwardIcon)), - (0, import_react32.createElement)("button", { -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react41.createElement)(BackIcon)), -+ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react41.createElement)(ForwardIcon)), -+ (0, import_react41.createElement)("button", { ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react42.createElement)(BackIcon)), ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react42.createElement)(ForwardIcon)), ++ (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", disabled: !page.url, @@ -15785,14 +19993,14 @@ diff --git a/client.js b/client.js onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload") - }, (0, import_react32.createElement)(page.loading ? StopIcon : ReloadIcon)), - (0, import_react32.createElement)( -+ }, (0, import_react41.createElement)(page.loading ? StopIcon : ReloadIcon)), -+ (0, import_react41.createElement)( ++ }, (0, import_react42.createElement)(page.loading ? StopIcon : ReloadIcon)), ++ (0, import_react42.createElement)( "form", { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 }, - (0, import_react32.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react32.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), - (0, import_react32.createElement)("input", { -+ (0, import_react41.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react41.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), -+ (0, import_react41.createElement)("input", { ++ (0, import_react42.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react42.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)), ++ (0, import_react42.createElement)("input", { ref: input, type: "text", spellCheck: false, @@ -15801,34 +20009,34 @@ diff --git a/client.js b/client.js }) ), - page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null -+ page.loading ? (0, import_react41.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null ++ page.loading ? (0, import_react42.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null ), - (0, import_react32.createElement)( -+ (0, import_react41.createElement)( ++ (0, import_react42.createElement)( "div", { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 }, - page.error ? (0, import_react32.createElement)( -+ page.error ? (0, import_react41.createElement)( ++ page.error ? (0, import_react42.createElement)( "div", { className: "bt-browser-empty" }, - (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), - (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), - page.url ? (0, import_react32.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { -+ (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), -+ (0, import_react41.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), -+ page.url ? (0, import_react41.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { ++ (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")), ++ (0, import_react42.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)), ++ page.url ? (0, import_react42.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => { void actions.browserNavigate(tabId, page.url); } }, t("Try again")) : null - ) : !page.url ? (0, import_react32.createElement)( -+ ) : !page.url ? (0, import_react41.createElement)( ++ ) : !page.url ? (0, import_react42.createElement)( "div", { className: "bt-browser-empty" }, - (0, import_react32.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), - (0, import_react32.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), - (0, import_react32.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) -+ (0, import_react41.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), -+ (0, import_react41.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), -+ (0, import_react41.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) ++ (0, import_react42.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }), ++ (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })), ++ (0, import_react42.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })) ) : null ) ); @@ -15844,36 +20052,36 @@ diff --git a/client.js b/client.js - const [viewport, setViewport] = (0, import_react32.useState)(() => window.innerWidth); - const [dragging, setDragging] = (0, import_react32.useState)(false); - const [resizing, setResizing] = (0, import_react32.useState)(false); -+ const root = (0, import_react41.useRef)(null); -+ const fly = (0, import_react41.useRef)(null); -+ const [landed, setLanded] = (0, import_react41.useState)(false); -+ const [settling, setSettling] = (0, import_react41.useState)(false); -+ const [width, setWidth] = (0, import_react41.useState)(savedWidth); -+ const [viewport, setViewport] = (0, import_react41.useState)(() => window.innerWidth); -+ const [dragging, setDragging] = (0, import_react41.useState)(false); -+ const [resizing, setResizing] = (0, import_react41.useState)(false); ++ const root = (0, import_react42.useRef)(null); ++ const fly = (0, import_react42.useRef)(null); ++ const [landed, setLanded] = (0, import_react42.useState)(false); ++ const [settling, setSettling] = (0, import_react42.useState)(false); ++ const [width, setWidth] = (0, import_react42.useState)(savedWidth); ++ const [viewport, setViewport] = (0, import_react42.useState)(() => window.innerWidth); ++ const [dragging, setDragging] = (0, import_react42.useState)(false); ++ const [resizing, setResizing] = (0, import_react42.useState)(false); const instant = dragging || resizing; - const [reading, setReading] = (0, import_react32.useState)(false); -+ const [reading, setReading] = (0, import_react41.useState)(false); ++ const [reading, setReading] = (0, import_react42.useState)(false); const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport)); - (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react41.useLayoutEffect)(() => { ++ (0, import_react42.useLayoutEffect)(() => { if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH); }, [view, width]); - (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react41.useLayoutEffect)(() => { ++ (0, import_react42.useLayoutEffect)(() => { chatRoom.hold(instant); }, [instant]); - const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth }); -+ const [last, setLast] = (0, import_react41.useState)({ view, width: panelWidth }); ++ const [last, setLast] = (0, import_react42.useState)({ view, width: panelWidth }); if (last.view !== view || last.width !== panelWidth) { setLast({ view, width: panelWidth }); if (last.view !== view) setSettling(last.width !== panelWidth && !instant); } - const shownView = (0, import_react32.useRef)(view); - (0, import_react32.useLayoutEffect)(() => { -+ const shownView = (0, import_react41.useRef)(view); -+ (0, import_react41.useLayoutEffect)(() => { ++ const shownView = (0, import_react42.useRef)(view); ++ (0, import_react42.useLayoutEffect)(() => { const switched = settling && shownView.current !== view; shownView.current = view; if (switched) chatRoom.hold(true); @@ -15881,12 +20089,12 @@ diff --git a/client.js b/client.js if (switched) chatRoom.hold(false); }, [view, panelWidth, leaving]); - (0, import_react32.useEffect)(() => () => { -+ (0, import_react41.useEffect)(() => () => { ++ (0, import_react42.useEffect)(() => () => { chatRoom.set(null); chatRoom.hold(false); }, []); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { let timer; const onResize = () => { setResizing(true); @@ -15895,34 +20103,34 @@ diff --git a/client.js b/client.js }; }, []); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { if (!settling) return void 0; const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40); return () => clearTimeout(timer); }, [settling, view]); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { const timer = setTimeout(() => setLanded(true), 360); return () => clearTimeout(timer); }, []); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { if (canBrowse) actions.browserEnsure(bot.id); }, [bot?.id, canBrowse]); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details"); }, [view, active, tabState.ids.length]); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { if (view !== "browser" || leaving) void actions.browserShow?.(null); }, [view, leaving]); - (0, import_react32.useEffect)(() => () => { -+ (0, import_react41.useEffect)(() => () => { ++ (0, import_react42.useEffect)(() => () => { void actions.browserShow?.(null); }, []); - (0, import_react32.useEffect)(() => { -+ (0, import_react41.useEffect)(() => { ++ (0, import_react42.useEffect)(() => { if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); @@ -15931,7 +20139,7 @@ diff --git a/client.js b/client.js }; } - (0, import_react32.useLayoutEffect)(() => { -+ (0, import_react41.useLayoutEffect)(() => { ++ (0, import_react42.useLayoutEffect)(() => { const move = fly.current; if (!move) return; fly.current = null; @@ -15940,7 +20148,7 @@ diff --git a/client.js b/client.js }; const hasStrip = canBrowse && tabState.ids.length > 0; - return (0, import_react32.createElement)( -+ return (0, import_react41.createElement)( ++ return (0, import_react42.createElement)( "aside", { ref: root, @@ -15949,19 +20157,19 @@ diff --git a/client.js b/client.js } }, - leaving ? null : (0, import_react32.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), -+ leaving ? null : (0, import_react41.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), ++ leaving ? null : (0, import_react42.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }), // Over the whole window while dragging: the cursor stays a resize cursor and the // pages underneath (the chat, the guest) see no pointer. - dragging ? (0, import_react_dom8.createPortal)((0, import_react32.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, - hasStrip ? (0, import_react32.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, - view === "browser" && bot ? (0, import_react32.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react32.createElement)( -+ dragging ? (0, import_react_dom8.createPortal)((0, import_react41.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, -+ hasStrip ? (0, import_react41.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, -+ view === "browser" && bot ? (0, import_react41.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react41.createElement)( ++ dragging ? (0, import_react_dom8.createPortal)((0, import_react42.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null, ++ hasStrip ? (0, import_react42.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null, ++ view === "browser" && bot ? (0, import_react42.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react42.createElement)( "div", { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" }, - (0, import_react32.createElement)(DetailsDrawer, { -+ (0, import_react41.createElement)(DetailsDrawer, { ++ (0, import_react42.createElement)(DetailsDrawer, { sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, @@ -15970,7 +20178,7 @@ diff --git a/client.js b/client.js `; +// src/client/remote-bot-settings.js -+var import_react42 = require("react"); ++var import_react43 = require("react"); +var REMOTE_BOT_SETTINGS_CSS = ` +.bt-remote-sheet-layer{position:fixed;inset:0;z-index:60;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none} +.bt-remote-sheet-layer[data-leaving]{pointer-events:none} @@ -15994,8 +20202,8 @@ diff --git a/client.js b/client.js +function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) { + const close = () => actions.closeSettings(); + useEscape(close); -+ const dialog = (0, import_react42.useRef)(null); -+ (0, import_react42.useEffect)(() => { ++ const dialog = (0, import_react43.useRef)(null); ++ (0, import_react43.useEffect)(() => { + const before = document.activeElement; + dialog.current?.focus?.({ preventScroll: true }); + return () => { @@ -16010,30 +20218,30 @@ diff --git a/client.js b/client.js + }; + if (!bot?.remote) return null; + const who = bot.remote.homeName || bot.remote.owner || bot.remote.home || t("Friend's Bot"); -+ return (0, import_react42.createElement)( ++ return (0, import_react43.createElement)( + "div", + { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" }, -+ (0, import_react42.createElement)("div", { className: "bt-scrim", onMouseDown: close }), -+ (0, import_react42.createElement)( ++ (0, import_react43.createElement)("div", { className: "bt-scrim", onMouseDown: close }), ++ (0, import_react43.createElement)( + "div", + { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown }, -+ (0, import_react42.createElement)( ++ (0, import_react43.createElement)( + "div", + { className: "bt-remote-sheet-head" }, -+ (0, import_react42.createElement)(BotMark, { bot, size: 40, live: true }), -+ (0, import_react42.createElement)( ++ (0, import_react43.createElement)(BotMark, { bot, size: 40, live: true }), ++ (0, import_react43.createElement)( + "div", + { className: "bt-remote-sheet-who" }, -+ (0, import_react42.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), -+ (0, import_react42.createElement)("span", { className: "bt-remote-sheet-sub" }, who) ++ (0, import_react43.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name), ++ (0, import_react43.createElement)("span", { className: "bt-remote-sheet-sub" }, who) + ), -+ (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react42.createElement)(CloseIcon)) ++ (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react43.createElement)(CloseIcon)) + ), -+ (0, import_react42.createElement)( ++ (0, import_react43.createElement)( + "div", + { className: "bt-remote-sheet-body" }, -+ bot.remote.handle ? (0, import_react42.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, -+ (0, import_react42.createElement)(FlyBotDetails, { bot, roster, actions }) ++ bot.remote.handle ? (0, import_react43.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null, ++ (0, import_react43.createElement)(FlyBotDetails, { bot, roster, actions }) + ) + ) + ); @@ -16041,7 +20249,7 @@ diff --git a/client.js b/client.js + // src/client/overlays.js -var import_react33 = require("react"); -+var import_react43 = require("react"); ++var import_react44 = require("react"); var import_react_dom9 = require("react-dom"); var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; @@ -16051,8 +20259,8 @@ diff --git a/client.js b/client.js const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); - const previous = (0, import_react33.useRef)(current); - (0, import_react33.useEffect)(() => { -+ const previous = (0, import_react43.useRef)(current); -+ (0, import_react43.useEffect)(() => { ++ const previous = (0, import_react44.useRef)(current); ++ (0, import_react44.useEffect)(() => { if (previous.current === current) return; previous.current = current; if (current) actions.followPanel?.(current); @@ -16060,8 +20268,8 @@ diff --git a/client.js b/client.js const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null; - const [kept, setKept] = (0, import_react33.useState)(open); - (0, import_react33.useEffect)(() => { -+ const [kept, setKept] = (0, import_react43.useState)(open); -+ (0, import_react43.useEffect)(() => { ++ const [kept, setKept] = (0, import_react44.useState)(open); ++ (0, import_react44.useEffect)(() => { if (open) { setKept(open); return void 0; @@ -16077,8 +20285,8 @@ diff --git a/client.js b/client.js - import_react33.Fragment, + const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS); + const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS); -+ return (0, import_react43.createElement)( -+ import_react43.Fragment, ++ return (0, import_react44.createElement)( ++ import_react44.Fragment, null, - pane ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, - ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, @@ -16088,17 +20296,17 @@ diff --git a/client.js b/client.js - menu ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, - settings ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, - memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null -+ pane ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, -+ ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, -+ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, -+ ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, -+ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, -+ menu ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, -+ settings ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, -+ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, -+ memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, -+ inbox ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, -+ board ? (0, import_react_dom9.createPortal)((0, import_react43.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null ++ pane ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null, ++ ui2.newChat ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null, ++ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null, ++ ui2.exchange ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null, ++ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null, ++ menu ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null, ++ settings ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null, ++ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null, ++ memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null, ++ inbox ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null, ++ board ? (0, import_react_dom9.createPortal)((0, import_react44.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null ); } @@ -16306,7 +20514,7 @@ diff --git a/client.js b/client.js // src/client/user-message.js -var import_react34 = require("react"); -+var import_react44 = require("react"); ++var import_react45 = require("react"); var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { @@ -16393,37 +20601,37 @@ diff --git a/client.js b/client.js - (0, import_react34.createElement)(BotMark, { bot: part.bot, size: 16 }), - part.label - ) : (0, import_react34.createElement)(import_react34.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references))); -+ const chip = (part) => [(0, import_react44.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; ++ const chip = (part) => [(0, import_react45.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; + return userTextParts(text, bots, referenceLabels).map((part, index) => { -+ if (!part.bot) return (0, import_react44.createElement)(import_react44.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)); ++ if (!part.bot) return (0, import_react45.createElement)(import_react45.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)); + const title = `@${part.bot.name}`; -+ return typeof part.bot.id === "string" ? (0, import_react44.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react44.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); ++ return typeof part.bot.id === "string" ? (0, import_react45.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react45.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); + }); } function Attachments({ attachments, renderMessageImages }) { const compact2 = attachments.length > 1; - return (0, import_react34.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { -+ return (0, import_react44.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { ++ return (0, import_react45.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { if (block.type === "image") { - return renderMessageImages ? (0, import_react34.createElement)(import_react34.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null; -+ return renderMessageImages ? (0, import_react44.createElement)(import_react44.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null; ++ return renderMessageImages ? (0, import_react45.createElement)(import_react45.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null; } const file = block.attachment; const meta = [ui.fileExtension?.(file.name)?.toUpperCase().slice(0, 8), ui.fileSizeText?.(file.bytes)].filter(Boolean).join(" · "); - return (0, import_react34.createElement)( -+ return (0, import_react44.createElement)( ++ return (0, import_react45.createElement)( "span", { key: index, className: "bt-ufile", title: file.name }, - ui.FileTypeIcon ? (0, import_react34.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, - (0, import_react34.createElement)("span", { className: "bt-ufile-text" }, (0, import_react34.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react34.createElement)("span", { className: "bt-ufile-meta" }, meta) : null) -+ ui.FileTypeIcon ? (0, import_react44.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, -+ (0, import_react44.createElement)("span", { className: "bt-ufile-text" }, (0, import_react44.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react44.createElement)("span", { className: "bt-ufile-meta" }, meta) : null) ++ ui.FileTypeIcon ? (0, import_react45.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, ++ (0, import_react45.createElement)("span", { className: "bt-ufile-text" }, (0, import_react45.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react45.createElement)("span", { className: "bt-ufile-meta" }, meta) : null) ); })); } function CopyButton({ text }) { - const [copied, setCopied] = (0, import_react34.useState)(false); -+ const [copied, setCopied] = (0, import_react44.useState)(false); ++ const [copied, setCopied] = (0, import_react45.useState)(false); const copy2 = () => { void navigator.clipboard?.writeText(text).then(() => { setCopied(true); @@ -16431,7 +20639,7 @@ diff --git a/client.js b/client.js }); }; - return (0, import_react34.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react34.createElement)(CheckIcon) : (0, import_react34.createElement)(CopyIcon)); -+ return (0, import_react44.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react44.createElement)(CheckIcon) : (0, import_react44.createElement)(CopyIcon)); ++ return (0, import_react45.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react45.createElement)(CheckIcon) : (0, import_react45.createElement)(CopyIcon)); } var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0; function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) { @@ -16447,7 +20655,7 @@ diff --git a/client.js b/client.js + const person = incoming && author.authorKind === "human" && !author.ownerName; + const faceMine = Boolean(room2?.origin === "face-to-face" && !incoming); + const myName = faceMine ? facePeople(room2, roster).find((entry) => entry.mine)?.name || cleanPeerName(roster.self?.displayName, "") || author?.name || "" : ""; -+ return (0, import_react44.createElement)( ++ return (0, import_react45.createElement)( "div", - { className: "bt-umsg", "data-steering": steering || void 0 }, - (0, import_react34.createElement)( @@ -16459,28 +20667,28 @@ diff --git a/client.js b/client.js + "data-author-kind": author?.authorKind, + "data-author-name": author?.name + }, -+ person ? (0, import_react44.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react44.createElement)(PersonMark, { name: author.name })) : null, -+ faceMine ? (0, import_react44.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react44.createElement)(PersonMark, { name: myName || void 0 })) : null, -+ (0, import_react44.createElement)( ++ person ? (0, import_react45.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react45.createElement)(PersonMark, { name: author.name })) : null, ++ faceMine ? (0, import_react45.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react45.createElement)(PersonMark, { name: myName || void 0 })) : null, ++ (0, import_react45.createElement)( "div", { className: "bt-ustack" }, - attachments.length > 0 ? (0, import_react34.createElement)(Attachments, { attachments, renderMessageImages }) : null, - text !== "" || rest.length > 0 ? (0, import_react34.createElement)( -+ faceMine && myName ? (0, import_react44.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react44.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react44.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, -+ attachments.length > 0 ? (0, import_react44.createElement)(Attachments, { attachments, renderMessageImages }) : null, -+ text !== "" || rest.length > 0 ? (0, import_react44.createElement)( ++ faceMine && myName ? (0, import_react45.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react45.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react45.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, ++ attachments.length > 0 ? (0, import_react45.createElement)(Attachments, { attachments, renderMessageImages }) : null, ++ text !== "" || rest.length > 0 ? (0, import_react45.createElement)( "div", { className: "bt-ubody", title: clock3(data.time) }, - (0, import_react34.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), - rest.map((block, index) => ui.JsonBlock ? (0, import_react34.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null) -+ (0, import_react44.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), -+ rest.map((block, index) => ui.JsonBlock ? (0, import_react44.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null) ++ (0, import_react45.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), ++ rest.map((block, index) => ui.JsonBlock ? (0, import_react45.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null) ) : null, - referenceLabels.length > 0 ? (0, import_react34.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null -+ referenceLabels.length > 0 ? (0, import_react44.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null ++ referenceLabels.length > 0 ? (0, import_react45.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null ), - text === "" ? null : (0, import_react34.createElement)("div", { className: "bt-utools" }, (0, import_react34.createElement)(CopyButton, { text })) -+ text === "" ? null : (0, import_react44.createElement)("div", { className: "bt-utools" }, (0, import_react44.createElement)(CopyButton, { text })) ++ text === "" ? null : (0, import_react45.createElement)("div", { className: "bt-utools" }, (0, import_react45.createElement)(CopyButton, { text })) ); } var USER_MESSAGE_CSS = ` @@ -16579,10 +20787,10 @@ diff --git a/client.js b/client.js + off(); + timers.clearTimeout?.(timer); + }; - } - ++} ++ +// src/client/stats-dock.js -+var import_react45 = require("react"); ++var import_react46 = require("react"); +var STATS_DOCK = "conversation.composer.dock"; +var STATS_ID = "stats"; +var STATS_IDS = [STATS_ID, "activity"]; @@ -16598,17 +20806,17 @@ diff --git a/client.js b/client.js +function statsDock(entriesOf, id = STATS_ID) { + const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id); + function StatsDock(props) { -+ const t2 = (0, import_react45.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); ++ const t2 = (0, import_react46.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); + const entry = shell(); + if (!entry || typeof props.usePerformanceUsage !== "function") return null; -+ return (0, import_react45.createElement)(entry.component, { ...props, t: t2 }); ++ return (0, import_react46.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 -+var import_react46 = require("react"); ++var import_react47 = require("react"); var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; function apply(ctx) { + const flybot = createClientApi2(); @@ -17021,7 +21229,7 @@ diff --git a/client.js b/client.js ]; + 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_react46.createElement)(TurnFailures, { ...props, work }) }); ++ const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react47.createElement)(TurnFailures, { ...props, work }) }); + return () => { + off(); + style.remove(); diff --git a/patches/ds-bot/files/src/client/delivery-chart.js b/patches/ds-bot/files/src/client/delivery-chart.js new file mode 100644 index 00000000..e9ff2131 --- /dev/null +++ b/patches/ds-bot/files/src/client/delivery-chart.js @@ -0,0 +1,243 @@ +// Delivery trend charts (#1212): one 7-day line per countable metric, drawn from `metrics.daily`. +// The host half builds the series (Asia/Shanghai days, the last entry carries `partial: true` until +// the day is over); this module only draws it, and a snapshot without the series says so instead of +// guessing. A day the host could not read breaks the line — an unknown is never drawn as zero. +import { createElement as h } from 'react' +import { t } from './i18n.js' + +export const CHART_WIDTH = 168 +export const CHART_HEIGHT = 28 +const PAD = 3 +const DOT_RADIUS = 2 +// The dot that is not a measurement yet (today, still running) is drawn a touch larger so the +// hollow ring stays visible at this size. +const HOLLOW_RADIUS = 2.5 +// The most days a chart draws; the host sends 7 today and a longer series is cut to its end. +export const CHART_DAYS = 7 + +// Keys line up with the rows `metricRows()` builds, in that order: the first five are countable and +// carry a line, the two medians after them cannot be added up and get no chart at all (#1212). +// The five countable metrics draw a line; the CI share draws one too but is a ratio, so it never +// takes a Σ total. Ties to the rows `metricRows()` builds, by key. +export const CHART_KEYS = ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci'] +export const COUNTING_KEYS = ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests'] + +// The days to draw, or null when the snapshot carries no series at all (host half not landed yet, +// or an older host) — that is the empty state, not seven zeros. +export function chartDays(daily) { + if (!Array.isArray(daily)) return null + const days = daily.filter(day => day && typeof day === 'object') + return days.length ? days.slice(-CHART_DAYS) : null +} + +// True while today is still running: the last day says `partial: true`. +export function todayPartial(daily) { + const days = chartDays(daily) + return Boolean(days && days.length && days[days.length - 1].partial === true) +} + +// One day's number for one metric; null means the host did not get it, which is not a zero. +export function dayValue(day, key) { + if (!day || typeof day !== 'object') return null + if (key === 'ci') { + const failed = day.ci?.failed + const counted = day.ci?.counted + const total = day.ci?.total + // A superseded run is not in the denominator when the host counted them (#1212 contract). + const denominator = Number.isFinite(counted) && counted > 0 + ? counted + : (Number.isFinite(total) && total > 0 ? total : null) + if (!Number.isFinite(failed) || denominator === null) return null + return Math.min(1, Math.max(0, failed / denominator)) + } + const value = day[key] + return Number.isFinite(value) ? value : null +} + +// The calendar date the day stands for, in the zone the host cuts days by (Asia/Shanghai, UTC+8, +// no DST). A start the host did not send leaves the date out of the title, never invents one. +export function dayLabel(day) { + // The contract is a plain date: 'YYYY-MM-DD'; an older answer carried an ISO start, read the + // same way so a host mid-upgrade still labels its days. + if (typeof day?.date === 'string' && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(day.date)) return day.date + const at = Date.parse(day?.start ?? '') + if (!Number.isFinite(at)) return '' + return new Date(at + 8 * 3_600_000).toISOString().slice(0, 10) +} + +// `2026-10-26 · 12`, and `2026-10-26 · 未取到` for a day the host could not read. +export function pointTitle(day, key) { + const date = dayLabel(day) + const value = dayValue(day, key) + const shown = value === null + ? t('not available') + : (key === 'ci' ? `${Math.round(value * 100)}%` : String(value)) + return date ? `${date} · ${shown}` : shown +} + +// How many of the drawn days carry a number for this metric; fewer than the window means the total +// is incomplete and wears the star (#1212). +export function knownDays(daily, key) { + const days = chartDays(daily) ?? [] + return days.filter(day => dayValue(day, key) !== null).length +} + +const round = value => Math.round(value * 100) / 100 + +// The SVG geometry for one metric: one x per day, a y for each known day, and the path split at +// every unknown day. Pure, so the shape can be asserted without React (#1212). +export function chartGeometry(daily, key, { width = CHART_WIDTH, height = CHART_HEIGHT } = {}) { + const days = chartDays(daily) + if (!days) return null + const values = days.map(day => dayValue(day, key)) + const known = values.filter(value => value !== null) + const top = known.length ? Math.max(...known, 0) : 0 + // All-zero days still sit on the floor; a flat line on the baseline is the honest picture, and a + // filled dot there is how a real zero stays apart from a missing day. + const span = top > 0 ? top : 1 + const step = days.length > 1 ? (width - PAD * 2) / (days.length - 1) : 0 + const dots = days.map((day, i) => { + const value = values[i] + const last = i === days.length - 1 + const state = value === null ? 'missing' : (last && day.partial === true ? 'partial' : 'full') + return { + index: i, + x: round(PAD + step * i), + y: value === null ? null : round(PAD + (height - PAD * 2) * (1 - value / span)), + today: last, + // Only the day that is not over yet is drawn hollow; a finished day is a filled dot. + hollow: state === 'partial', + partial: state === 'partial', + state, + title: pointTitle(day, key), + value, + } + }) + const segments = [] + let run = null + for (const dot of dots) { + if (dot.y === null) { + if (run) { segments.push(run); run = null } + continue + } + if (!run) run = { dots: [] } + run.dots.push(dot) + } + if (run) segments.push(run) + return { + width, + height, + dots, + known: known.length, + segments: segments.map(segment => ({ + d: segment.dots.map((dot, i) => `${i ? 'L' : 'M'}${dot.x} ${dot.y}`).join(' '), + // Only the stretch that ends today is the day that is not over yet. + dashed: segment.dots.some(dot => dot.partial), + today: segment.dots.some(dot => dot.today), + })), + } +} + +// One metric's line: 168×28, 1.5px stroke, r=2 dots, a `title` on every point. Each dot says its +// own state (`full` / `partial`); a missing day has no dot at all, so a break can never be read as +// a zero, and a real zero is a filled dot on the baseline (#1212). +export function DeliveryChart({ daily, metricKey, label }) { + const geo = chartGeometry(daily, metricKey) + if (!geo || !geo.known) return null + const name = label ? t('{label} over the last 7 days', { label }) : t('Last 7 days') + return h('svg', { + className: 'bt-del-chart', + 'data-flybot': 'delivery-chart', + 'data-metric': metricKey, + width: geo.width, + height: geo.height, + viewBox: `0 0 ${geo.width} ${geo.height}`, + role: 'img', + 'aria-label': name, + }, + geo.segments.map((segment, i) => h('path', { + key: `s${i}`, + className: 'bt-del-chart-line', + 'data-today': segment.today ? 'true' : undefined, + ...(segment.dashed ? { 'stroke-dasharray': '2 2' } : {}), + d: segment.d, + })), + geo.dots.filter(dot => dot.y !== null).map(dot => h('circle', { + key: `d${dot.index}`, + className: 'bt-del-chart-dot', + 'data-state': dot.state, + 'data-hollow': dot.hollow ? 'true' : undefined, + cx: dot.x, + cy: dot.y, + r: dot.hollow ? HOLLOW_RADIUS : DOT_RADIUS, + }, h('title', null, dot.title)))) +} + +// The metrics block #1212 draws: `label | line | today | 7 days`, one row per metric, the five +// countable lines and the CI share stacked. Rows keep the numbers `metricRows()` already formatted; a total is +// prefixed with Σ and muted so only one number per row is the day's, and it wears a star when the +// window has days the host could not read. The two median rows get no line, no placeholder and no +// total — a median cannot be added up. Below 900px the line moves under its row (DELIVERY_CHART_CSS). +export function DeliveryMetrics({ rows = [], daily }) { + const days = chartDays(daily) + const short = CHART_KEYS + .map(key => knownDays(days, key)) + .filter(count => count < (days?.length ?? 0)) + const fewest = short.length ? Math.min(...short) : 0 + const head = h('div', { className: 'bt-del-dm-head', 'data-flybot': 'delivery-metrics-head' }, + h('span', { className: 'bt-del-dm-label' }, ''), + h('span', { className: 'bt-del-dm-chart-head' }, ''), + h('span', { className: 'bt-del-dm-value' }, t('Today')), + h('span', { className: 'bt-del-dm-value bt-del-dm-week' }, t('7 days'))) + const body = rows.map((row, index) => { + // The row names the series it belongs to, so reordering the rows cannot hand one metric + // another metric's line; a row outside CHART_KEYS (a median) simply gets none. + const key = CHART_KEYS.includes(row?.key) ? row.key : null + const chart = days && key + ? h(DeliveryChart, { key: `c-${key}`, daily: days, metricKey: key, label: row?.label }) + : null + // A ratio is not a sum: the CI share keeps its week number plain, and the star marks an + // incomplete count only. + const counted = key !== null && COUNTING_KEYS.includes(key) + const star = days && counted && knownDays(days, key) < days.length ? '*' : '' + const cells = [ + h('span', { key: 'label', className: 'bt-del-dm-label' }, row?.label ?? ''), + // A median row gets no chart cell at all: no line, no placeholder box, no total. + ...(key ? [h('span', { key: 'chart', className: 'bt-del-dm-chart' }, chart)] : []), + h('span', { key: 'today', className: 'bt-del-dm-value bt-del-dm-today' }, row?.today ?? '—'), + h('span', { key: 'week', className: 'bt-del-dm-value bt-del-dm-week' }, counted ? `Σ ${row?.week ?? '—'}${star}` : (row?.week ?? '—')), + ] + return h('div', { key: row?.label ?? String(index), className: 'bt-del-dm-row', 'data-metric': key ?? undefined }, cells) + }) + return h('div', { className: 'bt-del-dm', 'data-flybot': 'delivery-metrics' }, + head, + body, + days ? null : h('p', { className: 'bt-del-chart-empty', 'data-flybot': 'delivery-chart-empty' }, t('No 7-day data yet.')), + days && short.length + ? h('p', { className: 'bt-del-chart-foot', 'data-flybot': 'delivery-chart-foot' }, t('* {count} of {days} days had data', { count: fewest, days: days.length })) + : null) +} + +export const DELIVERY_CHART_CSS = ` +.bt-del-dm{display:flex;flex-direction:column;gap:2px;min-width:0} +.bt-del-dm-head,.bt-del-dm-row{display:flex;align-items:center;gap:8px} +.bt-del-dm-head{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)} +.bt-del-dm-label{flex:1 1 auto;min-width:0;font-size:12px;line-height:16px;color:var(--bt-ink-2)} +.bt-del-dm-head .bt-del-dm-label{color:inherit} +.bt-del-dm-chart,.bt-del-dm-chart-head{flex:0 0 168px;display:flex;align-items:center;min-height:28px} +.bt-del-dm-value{flex:0 0 auto;min-width:56px;text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;color:var(--bt-ink)} +.bt-del-dm-head .bt-del-dm-value{color:inherit;font-weight:inherit} +.bt-del-dm-today{color:var(--bt-ink);font-weight:600} +.bt-del-dm-week{color:var(--bt-ink-3);font-weight:400} +.bt-del-chart{display:block;overflow:visible} +.bt-del-chart-line{fill:none;stroke:var(--bt-accent);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round} +.bt-del-chart-dot{fill:var(--bt-accent);stroke:none} +.bt-del-chart-dot[data-state=partial]{fill:none;stroke:var(--bt-accent);stroke-width:1.5} +.bt-del-chart-empty,.bt-del-chart-foot{margin:2px 0 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)} +@media (max-width:899.98px){ + .bt-del-dm-head{display:none} + .bt-del-dm-row{flex-wrap:wrap;gap:4px 8px;min-height:44px;align-content:center} + .bt-del-dm-chart{order:5;flex:0 0 100%;min-height:0} + .bt-del-dm-value{min-width:0} +} +` diff --git a/patches/ds-bot/files/src/client/delivery-view.js b/patches/ds-bot/files/src/client/delivery-view.js index 7c08b127..deedba57 100644 --- a/patches/ds-bot/files/src/client/delivery-view.js +++ b/patches/ds-bot/files/src/client/delivery-view.js @@ -8,6 +8,7 @@ // read. The poll is fast while a round is running (2 s) and slow once there is something to read. import { createElement as h, useEffect, useState } from 'react' import { t } from './i18n.js' +import { DeliveryMetrics, DELIVERY_CHART_CSS, todayPartial } from './delivery-chart.js' const POLL_MS = 60_000 // While the host is still building there is nothing to see, so ask often; once the board is up, @@ -129,17 +130,26 @@ export function metricRows(snapshot) { const today = snapshot?.today ?? {} const week = snapshot?.week ?? {} const ci = today.ci ?? null + // The CI share is a ratio of the runs that counted, so it is written as a percentage and is + // never summed; a window with nothing counted reads '—', not 0%. + const share = (entry) => (entry && Number.isFinite(entry.counted) && entry.counted > 0 + ? `${Math.round((entry.failed / entry.counted) * 100)}%` : '—') const count = (value) => (value === null || value === undefined ? '—' : String(value)) const rows = [ { label: t('New issues'), key: 'newIssues' }, { label: t('Closed issues'), key: 'closedIssues' }, { label: t('New pull requests'), key: 'newPullRequests' }, { label: t('Merged pull requests'), key: 'mergedPullRequests' }, + { key: 'ci', label: t('CI red share'), today: share(today.ci), week: share(week.ci) }, { label: t('Median issue close'), key: 'medianIssueCloseMs', format: duration }, { label: t('Median PR merge'), key: 'medianPullMergeMs', format: duration }, ].map(row => { + // The CI row already carries its own formatted share; every other row reads its number from + // today/week by key. + if (row.today !== undefined) return { key: row.key, label: row.label, today: row.today, week: row.week } const format = row.format ?? count - return { label: row.label, today: format(today[row.key]), week: format(week[row.key]) } + // `key` travels with the row so the chart finds its own series by name, not by position. + return { key: row.key, label: row.label, today: format(today[row.key]), week: format(week[row.key]) } }) return { dayStart: snapshot?.dayStart ?? null, @@ -266,17 +276,13 @@ function Diagnose({ onProbe }) { export function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, onProbe = null }) { const payload = view ?? snapshot if (!payload) return null - const { rows, ci, open } = metricRows(payload.metrics ?? payload) + const metricsPayload = payload.metrics ?? payload + const { rows, ci, open } = metricRows(metricsPayload) const error = payload.error ?? null const state = payload.state ?? 'fresh' return h('header', { className: 'bt-del-head' }, h('div', { className: 'bt-del-metrics' }, - h('table', { className: 'bt-del-table' }, - h('thead', null, h('tr', null, h('th', null, ''), h('th', null, t('Today')), h('th', null, t('7 days')))), - h('tbody', null, rows.map(row => h('tr', { key: row.label }, - h('th', { scope: 'row' }, row.label), - h('td', null, row.today), - h('td', null, row.week))))), + h(DeliveryMetrics, { rows, daily: metricsPayload.daily }), // An unreadable CI number is "unknown", never "0 of 0" (#1211). h('p', { className: 'bt-del-ci', 'data-ci': ci === null ? 'unknown' : 'read' }, ci === null ? t('CI not available this time') : t('CI failed {failed} of {total}', { failed: ci.failed, total: ci.total }), @@ -289,6 +295,7 @@ export function DeliveryHeader({ view, snapshot, repo = null, onRetry = null, on ? ` · ${t('median open age {age}', { age: duration(open.medianIssueAgeMs) })}` : '')), h('p', { className: 'bt-del-asof' }, state === 'loading' ? loadingText(payload.elapsedMs) : t('as of {time}', { time: timeOf(payload.asOf) }), + state !== 'loading' && todayPartial(metricsPayload.daily) ? ` · ${t('today is partial · as of {time}', { time: clockOf(payload.asOf) })}` : '', payload.refreshing ? ` · ${t('refreshing…')}` : '', error ? ` · ${hintFor(error, { repo })}` : '', onRetry ? h('button', { type: 'button', className: 'bt-soft bt-soft-sm bt-del-retry', onClick: onRetry }, t('Retry')) : null, @@ -428,4 +435,4 @@ export const DELIVERY_CSS = ` .bt-del-columns{flex-direction:column} .bt-del-column{min-width:0} } -` +` + DELIVERY_CHART_CSS diff --git a/patches/ds-bot/files/test/delivery-chart.test.js b/patches/ds-bot/files/test/delivery-chart.test.js new file mode 100644 index 00000000..013f8ee8 --- /dev/null +++ b/patches/ds-bot/files/test/delivery-chart.test.js @@ -0,0 +1,236 @@ +// The delivery trend chart (#1212): one x per day, a line that breaks at an unknown day instead of +// drawing through zero, a real zero as a filled dot on the baseline, today hollow only while it is +// still running, and no SVG at all when the snapshot carries no series. The chart is pure, so the +// geometry is asserted without a browser; the component tree is read the way delivery-view.test.js +// reads its view. +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' + +const bundle = await build({ + entryPoints: [fileURLToPath(new URL('../src/client/delivery-chart.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise, Number, Math, String, Array, Object, + require: (name) => { + if (name !== 'react') throw new Error(`no platform module ${name}`) + return { + createElement: (type, props, ...children) => { + const kids = children.flat(Infinity) + if (typeof type === 'function') return type({ ...(props ?? {}), children: kids }) + return { type, props: props ?? {}, children: kids } + }, + } + }, +}) +const { + CHART_WIDTH, CHART_HEIGHT, CHART_DAYS, CHART_KEYS, chartDays, chartGeometry, dayValue, + dayLabel, pointTitle, knownDays, todayPartial, DeliveryChart, DeliveryMetrics, DELIVERY_CHART_CSS, +} = module.exports + +const walk = (node, out = []) => { + if (node === null || typeof node !== 'object') return out + out.push(node) + for (const child of node.children ?? []) walk(child, out) + return out +} +const textOf = (node) => { + if (node === null || node === undefined || typeof node !== 'object') return String(node ?? '') + return [node.props?.title ?? '', ...(node.children ?? []).map(textOf)].join(' ') +} + +// Seven Asia/Shanghai days, 2026-10-20 … 2026-10-26; the last one is today and still running. The +// contract the host half is written against: { date, partial?, , ci: { failed, counted, total } }. +const daily = [ + { date: '2026-10-20', newIssues: 2, closedIssues: 1, newPullRequests: 3, mergedPullRequests: 0, ci: { failed: 1, total: 10, counted: 10 } }, + { date: '2026-10-21', newIssues: 4, closedIssues: 2, newPullRequests: 1, mergedPullRequests: 2, ci: { failed: 0, total: 8, counted: 8 } }, + { date: '2026-10-22', newIssues: 1, closedIssues: 3, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 3, total: 9, counted: 9 } }, + { date: '2026-10-23', newIssues: 6, closedIssues: 0, newPullRequests: 0, mergedPullRequests: 4, ci: { failed: 0, total: 0, counted: 0 } }, + { date: '2026-10-24', newIssues: 3, closedIssues: 5, newPullRequests: 4, mergedPullRequests: 2, ci: { failed: 2, total: 5, counted: 4 } }, + { date: '2026-10-25', newIssues: 0, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 3, ci: { failed: 1, total: 6, counted: 6 } }, + { date: '2026-10-26', newIssues: 5, closedIssues: 2, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 1, total: 4, counted: 4 }, partial: true }, +] + +test('#1212: seven days, the rightmost is today, and a longer series keeps its end', () => { + assert.equal(CHART_DAYS, 7) + const days = chartDays(daily) + assert.equal(days.length, 7) + assert.equal(dayLabel(days[6]), '2026-10-26', 'the contract carries the date itself') + assert.equal(dayLabel({ start: '2026-10-25T16:00:00.000Z' }), '2026-10-26', 'an older ISO start still labels a day') + assert.equal(chartDays(undefined), null, 'no series is null, not seven zeros') + assert.equal(chartDays([]), null) + assert.equal(chartDays([...daily, { date: '2026-10-27', newIssues: 9 }]).length, 7) + assert.equal(todayPartial(daily), true) + assert.equal(todayPartial(daily.map(day => ({ ...day, partial: undefined }))), false) +}) + +test('#1212: 168×28, one point per day, x across the box, the largest day at the top', () => { + assert.equal(CHART_WIDTH, 168) + assert.equal(CHART_HEIGHT, 28) + const geo = chartGeometry(daily, 'newIssues') + assert.equal(geo.width, CHART_WIDTH) + assert.equal(geo.height, CHART_HEIGHT) + assert.equal(geo.dots.length, 7) + const xs = geo.dots.map(dot => dot.x) + assert.deepEqual(xs, [...xs].sort((a, b) => a - b), 'x rises with the day') + assert.equal(xs[0], 3) + assert.equal(xs[6], CHART_WIDTH - 3) + assert.equal(Math.min(...geo.dots.map(dot => dot.y)), 3, 'the largest value sits on the top padding') + assert.equal(Math.max(...geo.dots.map(dot => dot.y)), CHART_HEIGHT - 3, 'a zero day sits on the bottom padding') + assert.equal(geo.segments.length, 1) + assert.equal(geo.dots[6].title, '2026-10-26 · 5') +}) + +test('#1212: an unknown day breaks the line instead of drawing through zero', () => { + const gapped = daily.map((item, i) => (i === 3 ? { ...item, newIssues: null } : item)) + const geo = chartGeometry(gapped, 'newIssues') + assert.equal(geo.dots[3].state, 'missing', 'the day is known to be missing') + assert.equal(geo.dots[3].y, null, 'and has no point') + assert.equal(geo.known, 6) + assert.equal(geo.segments.length, 2) + assert.equal(geo.segments[0].d.split('L').length, 3, 'first stretch: three days') + assert.equal(geo.segments[1].d.split('L').length, 3, 'second stretch: the three days after the gap') + assert.equal(geo.dots[3].title, '2026-10-23 · not available') + // A real zero connects and keeps a filled dot on the baseline: never the same picture as a gap. + const zeroed = daily.map((item, i) => (i === 3 ? { ...item, newIssues: 0 } : item)) + const flat = chartGeometry(zeroed, 'newIssues') + assert.equal(flat.segments.length, 1, 'a real zero still connects') + assert.equal(flat.dots[3].state, 'full') + assert.equal(flat.dots[3].y, CHART_HEIGHT - 3, 'the zero dot sits on the baseline') +}) + +test('#1212: today is hollow and dashed only while it is still running', () => { + const partial = chartGeometry(daily, 'newIssues') + assert.equal(partial.dots[6].state, 'partial') + assert.equal(partial.dots[6].hollow, true) + assert.equal(partial.segments[0].dashed, true) + assert.equal(partial.dots.filter(dot => dot.state === 'partial').length, 1, 'only today is partial') + const finished = chartGeometry(daily.map(day => ({ ...day, partial: undefined })), 'newIssues') + assert.equal(finished.dots[6].state, 'full', 'a finished day is a filled dot') + assert.equal(finished.dots[6].hollow, false) + assert.equal(finished.segments[0].dashed, false, 'and a solid line') +}) + +test('#1212: the CI metric is the red share of the runs that counted', () => { + assert.equal(dayValue(daily[0], 'ci'), 0.1) + assert.equal(dayValue(daily[4], 'ci'), 0.5, 'counted 4 of 5 runs: 2/4') + assert.equal(dayValue(daily[3], 'ci'), null, 'no runs that day is unknown, not 0%') + const geo = chartGeometry(daily, 'ci') + assert.equal(geo.dots[3].state, 'missing') + assert.equal(geo.dots[0].title, '2026-10-20 · 10%') + assert.equal(knownDays(daily, 'ci'), 6) +}) + +test('#1212: the component draws a 168×28 svg with a state and a title on every point', () => { + const node = DeliveryChart({ daily, metricKey: 'newIssues', label: 'New issues' }) + assert.equal(node.type, 'svg') + assert.equal(node.props.width, 168) + assert.equal(node.props.height, 28) + assert.equal(node.props.viewBox, '0 0 168 28') + const all = walk(node) + const circles = all.filter(item => item.type === 'circle') + assert.equal(circles.length, 7) + assert.equal(circles[6].props['data-state'], 'partial') + assert.equal(circles[6].props['data-hollow'], 'true') + assert.equal(circles[6].props.r, 2.5, 'the hollow today dot is drawn larger') + assert.equal(circles[0].props.r, 2, 'a measured day keeps the small dot') + assert.equal(circles[6].props.fill, undefined, 'hollow comes from CSS, not a hard-coded colour') + assert.equal(all.find(item => item.type === 'path').props['stroke-dasharray'], '2 2') + const titles = all.filter(item => item.type === 'title').map(item => item.children.join('')) + assert.equal(titles[6], '2026-10-26 · 5') + assert.equal(titles[3], '2026-10-23 · 6') + // A missing day is not a dot at all, so no circle can stand in for it. + const gapped = walk(DeliveryChart({ daily: daily.map((item, i) => (i === 3 ? { ...item, newIssues: null } : item)), metricKey: 'newIssues' })) + const gappedCircles = gapped.filter(item => item.type === 'circle') + assert.equal(gappedCircles.length, 6) + assert.equal(gappedCircles.some(circle => circle.props['data-state'] === 'missing'), false) + assert.equal(gappedCircles.filter(circle => circle.props['data-state'] === 'full').length, 5) +}) + +test('#1212: a metric with no readable day draws nothing', () => { + assert.equal(pointTitle({ date: '2026-10-23' }, 'newIssues'), '2026-10-23 · not available') + assert.equal(DeliveryChart({ daily: daily.map(item => ({ ...item, mergedPullRequests: null })), metricKey: 'mergedPullRequests' }), null) +}) + +test('#1212: one grey line instead of an empty chart when the snapshot has no series', () => { + const rows = CHART_KEYS.map(key => ({ key, label: key, today: '4', week: '30' })) + for (const missing of [undefined, null, [], 'nope']) { + const all = walk(DeliveryMetrics({ rows, daily: missing })) + assert.equal(all.some(item => item.type === 'svg'), false, 'no svg without data') + assert.equal(all.some(item => item.type === 'circle'), false) + const empty = all.find(item => item.props?.['data-flybot'] === 'delivery-chart-empty') + assert.ok(empty, 'the empty state is drawn') + assert.match(textOf(empty), /No 7-day data yet\.|7 天数据未取到/) + assert.equal(all.filter(item => item.props?.['data-metric'] && item.type === 'div').length, 5) + } +}) + +test('#1212: five lines, Σ totals, medians with no line and no total, 900px breakpoint', () => { + const rows = [ + { key: 'newIssues', label: 'New issues', today: '5', week: '21' }, + { key: 'closedIssues', label: 'Closed issues', today: '2', week: '14' }, + { key: 'newPullRequests', label: 'New pull requests', today: '2', week: '13' }, + { key: 'mergedPullRequests', label: 'Merged pull requests', today: '1', week: '13' }, + { key: 'ci', label: 'CI red share', today: '25%', week: '18%' }, + { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }, + { key: 'medianPullMergeMs', label: 'Median PR merge', today: '3h', week: '25h' }, + ] + const all = walk(DeliveryMetrics({ rows, daily })) + const rowNodes = all.filter(item => item.props?.className === 'bt-del-dm-row') + assert.equal(rowNodes.length, 7) + assert.equal(all.filter(item => item.type === 'svg').length, 5, 'the five countable metrics carry a line') + // The two median rows keep their numbers and get no line, no placeholder cell and no Σ. + const median = rowNodes[5] + assert.equal(median.children.length, 3, 'label, today, 7 days — no chart cell at all') + assert.equal(median.children.some(child => child.props.className === 'bt-del-dm-chart'), false) + assert.equal(median.children[2].children.join(''), '2h', 'a median is not summed') + assert.equal(rowNodes[6].children[2].children.join(''), '25h') + // One primary number per row: today is bold ink, the total is muted and prefixed. + assert.equal(rowNodes[0].children[2].props.className, 'bt-del-dm-value bt-del-dm-today') + assert.equal(rowNodes[0].children[3].props.className, 'bt-del-dm-value bt-del-dm-week') + assert.equal(rowNodes[0].children[3].children.join(''), 'Σ 21') + // A day the host could not read makes the total incomplete: it wears the star. + const gapped = walk(DeliveryMetrics({ rows, daily: daily.map((item, i) => (i === 2 ? { ...item, newIssues: null } : item)) })) + const gappedRows = gapped.filter(item => item.props?.className === 'bt-del-dm-row') + assert.equal(gappedRows[0].children[3].children.join(''), 'Σ 21*') + const foot = gapped.find(item => item.props?.['data-flybot'] === 'delivery-chart-foot') + assert.ok(foot, 'the star is explained') + assert.match(textOf(foot), /\* [0-9]+ of [0-9]+ days|天中/) + // Headers: the two value columns say today and the 7-day total; today names itself as partial. + const head = all.find(item => item.props?.['data-flybot'] === 'delivery-metrics-head') + const labels = head.children.map(child => (child.children ?? []).join('')) + // The partial day is named once, on the as-of line; the shared header keeps one word. + assert.equal(labels[2], 'Today') + assert.equal(labels[3], '7 days', 'no Σ in the shared header') + assert.match(DELIVERY_CHART_CSS, /max-width:899\.98px/, 'the line moves under its row below 900px') + assert.match(DELIVERY_CHART_CSS, /\.bt-del-dm-chart\{order:5;flex:0 0 100%/, 'the line takes its own line there') + assert.match(DELIVERY_CHART_CSS, /min-height:44px/, 'the row grows to 44px there') + assert.match(DELIVERY_CHART_CSS, /flex:0 0 168px/, 'the line does not shrink') + assert.match(DELIVERY_CHART_CSS, /stroke-width:1\.5/) + assert.match(DELIVERY_CHART_CSS, /fill:var\(--bt-accent\)/) + assert.equal(/#[0-9a-fA-F]{3,8}\b/.test(DELIVERY_CHART_CSS), false, 'no hard-coded colour') +}) + +test('#1212: a row finds its series by key, not by its position', () => { + const rows = [ + { key: 'ci', label: 'CI red share', today: '25%', week: '18%' }, + { key: 'newIssues', label: 'New issues', today: '5', week: '21' }, + { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }, + ] + const all = walk(DeliveryMetrics({ rows, daily })) + const rowNodes = all.filter(item => item.props?.className === 'bt-del-dm-row') + assert.deepEqual(rowNodes.map(node => node.props['data-metric']), ['ci', 'newIssues', undefined]) + assert.equal(rowNodes[0].children.length, 4, 'the countable row keeps its line') + assert.equal(rowNodes[0].children[2].children.join(''), '25%') + // A ratio is not a sum: the CI row keeps its number plain, while a count wears the star. + assert.equal(rowNodes[0].children[3].children.join(''), '18%', 'the CI share is a ratio, never summed') + assert.equal(rowNodes[1].children[3].children.join(''), 'Σ 21', 'the count keeps its Σ') + assert.equal(rowNodes[2].children.length, 3, 'a median row stays without a line, wherever it sits') + const svgs = all.filter(item => item.type === 'svg') + assert.equal(svgs.length, 2) +}) diff --git a/patches/ds-bot/files/test/delivery-view.test.js b/patches/ds-bot/files/test/delivery-view.test.js index 513b0285..cbc9cab9 100644 --- a/patches/ds-bot/files/test/delivery-view.test.js +++ b/patches/ds-bot/files/test/delivery-view.test.js @@ -161,9 +161,16 @@ test('five delivery columns, localized labels, badge wording (#1173)', () => { test('the header carries today, the 7-day comparison, the CI caveat and open ages (#1173)', () => { const rows = metricRows(snapshot.metrics) - assert.equal(rows.rows.length, 6) - assert.deepEqual({ ...rows.rows[0] }, { label: 'New issues', today: '4', week: '30' }) - assert.deepEqual({ ...rows.rows[4] }, { label: 'Median issue close', today: '1h', week: '2h' }) + // #1212 adds the CI red share as a seventh row: it draws a line like the counts, but as a ratio + // it never carries a Σ total. + assert.equal(rows.rows.length, 7) + // #1212: every row carries the metric key the chart looks its series up by, so the expectation + // names all six — the shape change is deliberate, not something to hide from the assertion. + assert.deepEqual({ ...rows.rows[0] }, { key: 'newIssues', label: 'New issues', today: '4', week: '30' }) + assert.deepEqual({ ...rows.rows[4] }, { key: 'ci', label: 'CI red share', today: '0%', week: '100%' }) + assert.deepEqual({ ...rows.rows[5] }, { key: 'medianIssueCloseMs', label: 'Median issue close', today: '1h', week: '2h' }) + assert.deepEqual([...rows.rows.map(row => row.key)], + ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci', 'medianIssueCloseMs', 'medianPullMergeMs']) assert.equal(rows.ci.failed, 2) assert.equal(rows.ci.total, 1200) assert.equal(rows.ci.caveat, true) @@ -171,6 +178,35 @@ test('the header carries today, the 7-day comparison, the CI caveat and open age assert.equal(rows.open.needsHuman, 4) }) +test('the CI red share reaches the page as a line, and the medians draw none (#1212)', () => { + // A key the table has no row for would draw no line: the CI share has to be a row, not a key only + // the chart knows about. + const daily = [ + { date: '2026-10-19', newIssues: 1, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 0, ci: { failed: 0, counted: 10 } }, + { date: '2026-10-20', newIssues: 2, closedIssues: 1, newPullRequests: 1, mergedPullRequests: 1, ci: { failed: 2, counted: 10 } }, + { date: '2026-10-21', newIssues: 0, closedIssues: 3, newPullRequests: 2, mergedPullRequests: 1, ci: { failed: 1, counted: 4 } }, + { date: '2026-10-22', newIssues: 4, closedIssues: 2, newPullRequests: 0, mergedPullRequests: 2, ci: { failed: null, counted: null } }, + { date: '2026-10-23', newIssues: 3, closedIssues: 3, newPullRequests: 1, mergedPullRequests: 1, ci: { failed: 0, counted: 8 } }, + { date: '2026-10-24', newIssues: 5, closedIssues: 4, newPullRequests: 3, mergedPullRequests: 2, ci: { failed: 3, counted: 12 } }, + { date: '2026-10-25', partial: true, newIssues: 4, closedIssues: 2, newPullRequests: 5, mergedPullRequests: 3, ci: { failed: 2, counted: 10 } }, + ] + const board = DeliveryBoard({ snapshot: { ...snapshot, metrics: { ...snapshot.metrics, daily } }, message: '' }) + const charts = walk(board).filter(node => node.props?.className === 'bt-del-chart') + assert.deepEqual([...charts.map(node => node.props['data-metric'])], + ['newIssues', 'closedIssues', 'newPullRequests', 'mergedPullRequests', 'ci']) + const rows = walk(board).filter(node => node.props?.className === 'bt-del-dm-row') + const ciRow = rows.find(node => node.props['data-metric'] === 'ci') + assert.ok(ciRow, 'the CI share is a row of the table, not only a chart key') + // A share of the week, never a sum. + assert.match(text(ciRow), /100%/) + assert.equal(text(ciRow).includes('Σ'), false) + // Medians cannot be added up and get no chart at all. + for (const key of ['medianIssueCloseMs', 'medianPullMergeMs']) { + const row = rows.find(node => node.props['data-metric'] === key) + assert.equal(walk(row).some(node => node.props?.className === 'bt-del-chart'), false) + } +}) + test('the board renders all five columns and a card with labels, owner and link (#1173)', () => { const board = DeliveryBoard({ snapshot: { ...snapshot, columns: snapshot.columns }, message: '' }) const sections = walk(board).filter(node => node.type === 'section' && node.props?.['data-column'])