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'])