diff --git a/changelog.d/1044-dead-turn-visible.fixed.md b/changelog.d/1044-dead-turn-visible.fixed.md
new file mode 100644
index 000000000..839ef7b89
--- /dev/null
+++ b/changelog.d/1044-dead-turn-visible.fixed.md
@@ -0,0 +1 @@
+A turn that dies before writing anything is no longer invisible: when a model call is refused (a provider budget cap, a 429), the chat shows one line naming the provider's own error above the composer, and a run of them says how many turns in a row died, so a quota outage reads as an outage instead of a Bot that chose to stay silent. A turn that already wrote text adds no line, and a completed turn clears the count ([#1044](https://github.com/ThinkFlowLab/FlyBot/issues/1044)).
diff --git a/core/client.js b/core/client.js
index 5619f3f5d..2a64bde98 100644
--- a/core/client.js
+++ b/core/client.js
@@ -64,6 +64,10 @@ var ZH = {
"Enter a number, or paste a group invite link": "输入数字面对面建群,或粘贴邀请链接加入",
"Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.": "选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。",
"Or paste a group invite link (https://…/r/…)": "或粘贴群邀请链接(https://…/r/…)",
+ // #1044: a turn that died before writing anything still says so where the chat is.
+ "Could not run: {error}": "这一轮没能跑起来:{error}",
+ "the model call failed": "模型调用失败",
+ "{count} turns in a row": "连续 {count} 轮",
"Group invite link": "群邀请链接",
"Room-only: friends do not see these Bots unless you invite them.": "仅限这个房间:除非你邀请,好友看不到这些 Bot。",
"6 digits": "6 位数字",
@@ -2020,8 +2024,8 @@ function bodyOf(def, paint, flat = false) {
return "";
}
}
-var eyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
+var eyeMarkup = ([cx, cy, w, h45, tilt]) => ``;
+var happyEyeMarkup = ([cx, cy, w, h45, 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%";
@@ -2717,29 +2721,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: h43, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
- if (!preview || typeof h43 !== "function") return null;
+function RoomInvitePreview({ h: h45, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
+ if (!preview || typeof h45 !== "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 h43(
+ return h45(
"div",
{ className: "fs-preview", "data-flybot": at, role: "status" },
- h43("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
- preview.lead ? h43("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
- preview.people ? h43("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
- preview.expiresAt ? h43("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
- onJoin || onClear ? h43(
+ h45("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
+ preview.lead ? h45("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
+ preview.people ? h45("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
+ preview.expiresAt ? h45("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
+ onJoin || onClear ? h45(
"div",
{ className: "fs-preview-foot", "data-flybot": child("foot") },
- showClear && onClear ? h43("button", {
+ showClear && onClear ? h45("button", {
type: "button",
className: "fs-btn fs-btn-sm",
"data-flybot": child("clear"),
disabled: !!busy,
onClick: onClear
}, clearLabel) : null,
- onJoin ? h43("button", {
+ onJoin ? h45("button", {
type: "button",
className: "fs-btn fs-btn-sm fs-btn-primary",
"data-flybot": child("join"),
@@ -3097,6 +3101,47 @@ var FLYBOT_CSS = `
.bt-flybot-pointer button { border: 0; border-radius: 999px; padding: 3px 10px; font-size: 12px; cursor: pointer; background: var(--bt-accent, #2563eb); color: #fff; }
`;
+// src/shared/turn-failures.js
+var FAILURE_LINES_SHOWN = 3;
+var failureLines = (snapshot, place, t2, limit = FAILURE_LINES_SHOWN) => {
+ const ended = Array.isArray(snapshot?.ended) ? snapshot.ended : [];
+ return ended.filter((entry) => entry?.silent === true && (entry.roomId ?? entry.sessionId) === place).slice(-limit).map((entry) => ({
+ id: `turn-failure-${entry.id}`,
+ text: t2("Could not run: {error}", { error: entry.failed ?? t2("the model call failed") }),
+ ...Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {}
+ }));
+};
+
+// src/client/turn-failures.js
+var import_react3 = require("react");
+function TurnFailures({ sessionId, work }) {
+ const [snapshot, setSnapshot] = (0, import_react3.useState)(() => work?.getSnapshot?.());
+ (0, import_react3.useEffect)(() => {
+ if (typeof work?.subscribe !== "function") return void 0;
+ const read = () => setSnapshot(work.getSnapshot());
+ const off = work.subscribe(read);
+ read();
+ return off;
+ }, [work]);
+ const lines = failureLines(snapshot, sessionId, t);
+ if (lines.length === 0) return null;
+ return (0, import_react3.createElement)(
+ "div",
+ { className: "bt-turn-failures", "data-flybot": "turn-failures" },
+ lines.map((line2) => (0, import_react3.createElement)(
+ "div",
+ { key: line2.id, className: "bt-turn-failure", "data-flybot": "turn-failure" },
+ (0, import_react3.createElement)("span", { className: "bt-turn-failure-text" }, line2.text),
+ line2.streak === void 0 ? null : (0, import_react3.createElement)("span", { className: "bt-turn-failure-streak" }, ` ${t("{count} turns in a row", { count: line2.streak })}`)
+ ))
+ );
+}
+var TURN_FAILURES_CSS = `
+.bt-turn-failures{width:100%;max-width:min(992px,100%);margin:0 auto;padding:0 0 6px;box-sizing:border-box}
+.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}
+`;
+
// src/client/themes.js
var THEME_KEYS = ["bg", "sidebar", "bubble", "card", "text", "user", "userText", "accent"];
var THEMES = {
@@ -3230,7 +3275,7 @@ function mentionCss(bots) {
}
// src/client/motion.js
-var import_react3 = require("react");
+var import_react4 = require("react");
var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
function springEasing(stiffness, damping) {
const w0 = Math.sqrt(stiffness);
@@ -3263,12 +3308,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 h43 = dt / steps;
+ const h45 = 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) * h43;
- value.x += value.v * h43;
+ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h45;
+ value.x += value.v * h45;
}
if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
value.x = value.to;
@@ -3303,10 +3348,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_react3.useRef)(null);
+ const ownBox = (0, import_react4.useRef)(null);
const box = given ?? ownBox;
- const thumb = (0, import_react3.useRef)(null);
- const own = (0, import_react3.useRef)(null);
+ const thumb = (0, import_react4.useRef)(null);
+ const own = (0, import_react4.useRef)(null);
if (own.current === null) {
own.current = {
key: void 0,
@@ -3340,7 +3385,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_react3.useLayoutEffect)(() => {
+ (0, import_react4.useLayoutEffect)(() => {
const state = own.current;
const node = box.current;
if (node === state.node && key === state.key) return;
@@ -3354,7 +3399,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai
}
place(fresh);
});
- (0, import_react3.useEffect)(() => () => {
+ (0, import_react4.useEffect)(() => () => {
own.current.observer?.disconnect();
own.current.springs.stop();
}, []);
@@ -3362,16 +3407,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_react3.createElement)(
+ return (0, import_react4.createElement)(
"div",
{ ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label },
- (0, import_react3.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
children
);
}
function useFlip(list, key, selector, id = (node) => node.dataset.flip) {
- const tops = (0, import_react3.useRef)(null);
- (0, import_react3.useLayoutEffect)(() => {
+ const tops = (0, import_react4.useRef)(null);
+ (0, import_react4.useLayoutEffect)(() => {
const container = list.current;
if (!container) return;
const next = /* @__PURE__ */ new Map();
@@ -3392,8 +3437,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) {
}, [key]);
}
function useMorph(box, key) {
- const last = (0, import_react3.useRef)({ key, height: null });
- (0, import_react3.useLayoutEffect)(() => {
+ const last = (0, import_react4.useRef)({ key, height: null });
+ (0, import_react4.useLayoutEffect)(() => {
const node = box.current;
const state = last.current;
if (!node) {
@@ -3421,8 +3466,8 @@ function useMorph(box, key) {
});
}
function useLinger(value, ms) {
- const [kept, setKept] = (0, import_react3.useState)(value);
- (0, import_react3.useEffect)(() => {
+ const [kept, setKept] = (0, import_react4.useState)(value);
+ (0, import_react4.useEffect)(() => {
if (value) {
setKept(value);
return void 0;
@@ -4664,66 +4709,66 @@ function installBrandChrome() {
}
// src/client/icons.js
-var import_react4 = require("react");
+var import_react5 = require("react");
function PlusIcon() {
- return (0, import_react4.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
+ return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
}
function SearchIcon() {
- return (0, import_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react4.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
+ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
}
function PeopleIcon() {
- return (0, import_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react4.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
- (0, import_react4.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_react4.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
+ (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
+ (0, import_react5.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
+ (0, import_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
);
}
function ArrowUpIcon() {
- return (0, import_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
+ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
}
function MicIcon({ size = 16 }) {
- return (0, import_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react4.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
- (0, import_react4.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
+ (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
+ (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
);
}
function WaveIcon({ size = 16 }) {
- return (0, import_react4.createElement)(
+ 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_react4.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
+ [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
);
}
function ShareIcon() {
- return (0, import_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
- (0, import_react4.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
+ (0, import_react5.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
);
}
function CloseIcon() {
- return (0, import_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
+ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
}
-var ChevronLeftIcon = () => (0, import_react4.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_react4.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" }));
-var ChevronRightIcon = () => (0, import_react4.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_react4.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_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
- paths.map((d, index) => (0, import_react4.createElement)("path", { key: index, d }))
+ paths.map((d, index) => (0, import_react5.createElement)("path", { key: index, d }))
);
};
var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5";
function CloudDownloadIcon({ done = false }) {
- return (0, import_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
- (0, import_react4.createElement)("path", { d: CLOUD }),
- done ? (0, import_react4.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react4.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react4.createElement)("path", { d: "M10 9.75v7" }), (0, import_react4.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" }))
+ (0, import_react5.createElement)("path", { d: CLOUD }),
+ done ? (0, import_react5.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react5.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react5.createElement)("path", { d: "M10 9.75v7" }), (0, import_react5.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" }))
);
}
var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]);
@@ -4742,14 +4787,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_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
- (0, import_react4.createElement)(
+ (0, import_react5.createElement)(
"g",
{ transform: "rotate(45 10 10)" },
- (0, import_react4.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_react4.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
+ (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
+ (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
)
);
}
@@ -4771,38 +4816,38 @@ var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-
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_react4.createElement)(
+ return (0, import_react5.createElement)(
"svg",
{ className: "bt-flybot-mark", width: size, height: size, viewBox: "0 0 512 512", "aria-hidden": true },
- (0, import_react4.createElement)(
+ (0, import_react5.createElement)(
"g",
{ className: "bt-flybot-wing" },
- (0, import_react4.createElement)("path", { d: FLYBOT_WING }),
- (0, import_react4.createElement)("path", { d: FLYBOT_WING_LOW })
+ (0, import_react5.createElement)("path", { d: FLYBOT_WING }),
+ (0, import_react5.createElement)("path", { d: FLYBOT_WING_LOW })
),
- (0, import_react4.createElement)(
+ (0, import_react5.createElement)(
"g",
{ className: "bt-flybot-wing", transform: "translate(512 0) scale(-1 1)" },
- (0, import_react4.createElement)("path", { d: FLYBOT_WING }),
- (0, import_react4.createElement)("path", { d: FLYBOT_WING_LOW })
+ (0, import_react5.createElement)("path", { d: FLYBOT_WING }),
+ (0, import_react5.createElement)("path", { d: FLYBOT_WING_LOW })
),
- (0, import_react4.createElement)("path", { className: "bt-flybot-body", d: FLYBOT_BODY }),
- (0, import_react4.createElement)("rect", { className: "bt-flybot-eye", x: 196, y: 222, width: 34, height: 58, rx: 17 }),
- (0, import_react4.createElement)("rect", { className: "bt-flybot-eye", x: 282, y: 222, width: 34, height: 58, rx: 17 }),
- (0, import_react4.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }),
- (0, import_react4.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 })
+ (0, import_react5.createElement)("path", { className: "bt-flybot-body", d: FLYBOT_BODY }),
+ (0, import_react5.createElement)("rect", { className: "bt-flybot-eye", x: 196, y: 222, width: 34, height: 58, rx: 17 }),
+ (0, import_react5.createElement)("rect", { className: "bt-flybot-eye", x: 282, y: 222, width: 34, height: 58, rx: 17 }),
+ (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }),
+ (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 })
);
}
// src/client/questions.js
-var import_react5 = require("react");
+var import_react6 = require("react");
var keyLetter = (index) => String.fromCharCode(65 + index);
var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
function useOptionKeys(count, onPick, ownRef) {
- const pick2 = (0, import_react5.useRef)(onPick);
+ const pick2 = (0, import_react6.useRef)(onPick);
pick2.current = onPick;
- (0, import_react5.useEffect)(() => {
+ (0, import_react6.useEffect)(() => {
if (count === 0) return void 0;
const onKey = (event) => {
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return;
@@ -4822,10 +4867,10 @@ function useOptionKeys(count, onPick, ownRef) {
}, [count, ownRef]);
}
function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) {
- const [own, setOwn] = (0, import_react5.useState)("");
- const [picked, setPicked] = (0, import_react5.useState)([]);
- const ownRef = (0, import_react5.useRef)(null);
- const titleId = (0, import_react5.useId)();
+ const [own, setOwn] = (0, import_react6.useState)("");
+ const [picked, setPicked] = (0, import_react6.useState)([]);
+ const ownRef = (0, import_react6.useRef)(null);
+ const titleId = (0, import_react6.useId)();
const options = optionsOf(question);
const multi = question.multiSelect === true;
const custom = own.trim();
@@ -4843,7 +4888,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_react5.createElement)("textarea", {
+ const field = ownAnswer ? (0, import_react6.createElement)("textarea", {
ref: ownRef,
rows: 1,
value: own,
@@ -4862,7 +4907,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_react5.createElement)(
+ return (0, import_react6.createElement)(
"button",
{
key: option.label,
@@ -4872,36 +4917,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised
"aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(),
onClick: () => choose(option.label)
},
- (0, import_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
- (0, import_react5.createElement)(
+ (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
+ (0, import_react6.createElement)(
"span",
{ className: "bt-q-body" },
- (0, import_react5.createElement)("span", { className: "bt-q-label" }, option.label),
- option.description ? (0, import_react5.createElement)("span", { className: "bt-q-desc" }, option.description) : null
+ (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label),
+ option.description ? (0, import_react6.createElement)("span", { className: "bt-q-desc" }, option.description) : null
),
- selected && !raised ? (0, import_react5.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react5.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_react5.createElement)(
+ rows.push((0, import_react6.createElement)(
"label",
{ key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 },
- ownLetter ? (0, import_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
- (0, import_react5.createElement)("span", { className: "bt-q-body" }, field)
+ ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
+ (0, import_react6.createElement)("span", { className: "bt-q-body" }, field)
));
}
let foot = null;
if (raised && multi) {
- foot = (0, import_react5.createElement)(
+ foot = (0, import_react6.createElement)(
"div",
{ className: "bt-q-foot" },
- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
- (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
+ (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
+ (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
);
} else if ((raised || multi) && canSubmit) {
- foot = (0, import_react5.createElement)("div", { className: "bt-q-foot" }, (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
+ foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
}
- return (0, import_react5.createElement)(
+ return (0, import_react6.createElement)(
"form",
{
className: raised ? "bt-q bt-q-raised" : "bt-q",
@@ -4911,24 +4956,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised
submit();
}
},
- (0, import_react5.createElement)(
+ (0, import_react6.createElement)(
"div",
{ className: "bt-q-head" },
- (0, import_react5.createElement)(
+ (0, import_react6.createElement)(
"div",
{ className: "bt-q-text" },
- (0, import_react5.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
- question.detail ? (0, import_react5.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
- raised && multi ? (0, import_react5.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
+ (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
+ question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
+ raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
),
- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react5.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_react5.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
- field && !raised ? (0, import_react5.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_react5.createElement)("label", { className: "bt-q-field" }, field),
- !multi && canSubmit ? (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
+ (0, import_react6.createElement)("label", { className: "bt-q-field" }, field),
+ !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
) : null,
foot
);
@@ -4938,29 +4983,29 @@ function SettledQuestion({ question, answer }) {
const prompt = question?.question ?? "";
const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []];
if (chosen.length === 0) {
- return (0, import_react5.createElement)(
+ return (0, import_react6.createElement)(
"div",
{ className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt },
- (0, import_react5.createElement)(
+ (0, import_react6.createElement)(
"div",
{ className: "bt-q-head" },
- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)("p", { className: "bt-q-title" }, prompt)),
- (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
+ (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)),
+ (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
)
);
}
- return (0, import_react5.createElement)(
+ return (0, import_react6.createElement)(
"div",
{ className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
- prompt ? (0, import_react5.createElement)("div", { className: "bt-q-head" }, (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
- (0, import_react5.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
+ prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
+ (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
const optionIndex = options.findIndex((option) => option.label === label);
- return (0, import_react5.createElement)(
+ return (0, import_react6.createElement)(
"div",
{ key: `${label}-${index}`, className: "bt-q-opt" },
- optionIndex >= 0 ? (0, import_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
- (0, import_react5.createElement)("span", { className: "bt-q-body" }, (0, import_react5.createElement)("span", { className: "bt-q-label" }, label)),
- (0, import_react5.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react5.createElement)(CheckIcon))
+ optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
+ (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)),
+ (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon))
);
}))
);
@@ -4968,8 +5013,8 @@ function SettledQuestion({ question, answer }) {
function QuestionCard({ matched }) {
const pending = matched;
const questions = pending.questions ?? [];
- const [index, setIndex] = (0, import_react5.useState)(0);
- const [answers, setAnswers] = (0, import_react5.useState)([]);
+ const [index, setIndex] = (0, import_react6.useState)(0);
+ const [answers, setAnswers] = (0, import_react6.useState)([]);
const question = questions[index];
if (question === void 0) return null;
const onAnswer = (answer) => {
@@ -4981,10 +5026,10 @@ function QuestionCard({ matched }) {
}
pending.answer({ answers: next });
};
- return (0, import_react5.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+ return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
}
function PendingQuestion({ sessionId, question, actions, inline }) {
- const [answered, setAnswered] = (0, import_react5.useState)(null);
+ const [answered, setAnswered] = (0, import_react6.useState)(null);
if (answered === question.id) return null;
const onAnswer = (answer) => {
setAnswered(question.id);
@@ -4994,10 +5039,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) {
setAnswered(question.id);
void actions.dismissQuestion(sessionId);
};
- return (0, import_react5.createElement)(
+ return (0, import_react6.createElement)(
"div",
{ className: inline ? "bt-q-inline" : "bt-q-dock" },
- (0, import_react5.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
+ (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
);
}
function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
@@ -5005,16 +5050,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_react5.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
+ return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
}
function AnsweredCell({ node }) {
const data = node.data ?? {};
const questions = Array.isArray(data.questions) ? data.questions : [];
if (questions.length === 0) {
- return data.text ? (0, import_react5.createElement)("div", { className: "bt-urow" }, (0, import_react5.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
+ return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
}
const answers = Array.isArray(data.answers) ? data.answers : [];
- return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react5.createElement)(SettledQuestion, {
+ return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, {
key: question.id,
question,
answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
@@ -5022,26 +5067,26 @@ function AnsweredCell({ node }) {
}
// src/client/secret-card.js
-var import_react6 = require("react");
+var import_react7 = require("react");
var MIN_VALUE = 4;
var NONE = {};
-var KeyIcon = () => (0, import_react6.createElement)(
+var KeyIcon = () => (0, import_react7.createElement)(
"svg",
{ width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
- (0, import_react6.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
- (0, import_react6.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
+ (0, import_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
+ (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
);
-var LockIcon = () => (0, import_react6.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_react6.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
- (0, import_react6.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
+ (0, import_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
+ (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
);
-var WarnIcon = () => (0, import_react6.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_react6.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
- (0, import_react6.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
+ (0, import_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
+ (0, import_react7.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
);
var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
@@ -5052,7 +5097,7 @@ var scopeLabel = (scope, roster) => {
};
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_react6.createElement)("input", {
+ return (0, import_react7.createElement)("input", {
className: "bt-secret-input",
type: MASK_BY_CSS ? "text" : "password",
"data-masked": MASK_BY_CSS ? "" : void 0,
@@ -5078,11 +5123,11 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
}
function ScopeSwitch({ value, onChange, bot }) {
const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]];
- return (0, import_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
- (0, import_react6.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
- options.map(([id, text]) => (0, import_react6.createElement)("button", {
+ (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",
@@ -5093,26 +5138,26 @@ function ScopeSwitch({ value, onChange, bot }) {
);
}
function SecretHead({ bot, title, detail, name, onClose }) {
- return (0, import_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-q-head bt-secret-head" },
- (0, import_react6.createElement)("span", { className: "bt-secret-who" }, (0, import_react6.createElement)(BotMark, { bot, size: 28 }), (0, import_react6.createElement)("span", { className: "bt-secret-lock" }, (0, import_react6.createElement)(LockIcon))),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))),
+ (0, import_react7.createElement)(
"div",
{ className: "bt-q-text" },
- (0, import_react6.createElement)("p", { className: "bt-q-title" }, title),
- (0, import_react6.createElement)("div", { className: "bt-q-detail" }, (0, import_react6.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
+ (0, import_react7.createElement)("p", { className: "bt-q-title" }, title),
+ (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
),
- onClose ? (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react6.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_react6.useState)("");
- const [scope, setScope] = (0, import_react6.useState)("all");
- const [replacing, setReplacing] = (0, import_react6.useState)(false);
- const [busy, setBusy] = (0, import_react6.useState)(false);
- const [error, setError] = (0, import_react6.useState)("");
- const noteId = (0, import_react6.useId)();
+ const [value, setValue] = (0, import_react7.useState)("");
+ const [scope, setScope] = (0, import_react7.useState)("all");
+ const [replacing, setReplacing] = (0, import_react7.useState)(false);
+ const [busy, setBusy] = (0, import_react7.useState)(false);
+ const [error, setError] = (0, import_react7.useState)("");
+ const noteId = (0, import_react7.useId)();
const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name);
const run = (promise) => {
setBusy(true);
@@ -5128,73 +5173,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_react6.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react6.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null;
- return (0, import_react6.createElement)(
+ const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null;
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind },
- (0, import_react6.createElement)(SecretHead, {
+ (0, import_react7.createElement)(SecretHead, {
bot,
title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }),
detail: request.purpose,
name: request.name,
onClose: busy ? void 0 : cancel
}),
- request.kind === "allow" ? (0, import_react6.createElement)("div", { className: "bt-secret-saved" }, (0, import_react6.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null,
- filling ? (0, import_react6.createElement)(
+ request.kind === "allow" ? (0, import_react7.createElement)("div", { className: "bt-secret-saved" }, (0, import_react7.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null,
+ filling ? (0, import_react7.createElement)(
"div",
{ className: "bt-secret-body" },
- (0, import_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })),
- request.kind === "fill" ? (0, import_react6.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null
+ (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })),
+ request.kind === "fill" ? (0, import_react7.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null
) : null,
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
"div",
{ className: "bt-secret-note", id: noteId },
t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }),
- (0, import_react6.createElement)("code", null, `$DSH_SECRET_${request.name}`),
+ (0, import_react7.createElement)("code", null, `$DSH_SECRET_${request.name}`),
".",
filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : ""
),
warn,
- error ? (0, import_react6.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null,
- (0, import_react6.createElement)(
+ 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_react6.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
- (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
- filling ? (0, import_react6.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react6.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
+ request.kind === "allow" && !replacing ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
+ filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
)
);
}
function 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_react6.createElement)(
+ 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_react6.createElement)(
+ (0, import_react7.createElement)(
"div",
{ className: "bt-q-head" },
- (0, import_react6.createElement)("span", { className: "bt-secret-who" }, (0, import_react6.createElement)(BotMark, { bot, size: 22 })),
- (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)(
+ (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 22 })),
+ (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)(
"p",
{ className: "bt-q-title" },
text,
- canceled ? null : (0, import_react6.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
+ canceled ? null : (0, import_react7.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
)),
- canceled ? (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react6.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react6.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_react6.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react6.createElement)(SettledSecret, { bot, request, roster });
+ return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster });
}
function 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_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(RequestView, { bot, request, roster, actions }));
+ if (request) return (0, import_react7.createElement)("div", { className: "bt-q-inline" }, (0, import_react7.createElement)(RequestView, { bot, request, roster, actions }));
if (done && /already saved/.test(contentText(content))) {
- return (0, import_react6.createElement)("div", { className: "bt-event" }, (0, import_react6.createElement)("span", { className: "bt-secret-event" }, (0, import_react6.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
+ return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
}
return null;
}
@@ -5207,46 +5252,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_react6.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react6.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
+ return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
}
var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some";
function ScopeEditor({ secret, roster, onSave }) {
- const [mode, setMode] = (0, import_react6.useState)(scopeMode(secret.scope));
- const [picked, setPicked] = (0, import_react6.useState)(secret.scope === "all" ? [] : secret.scope);
+ const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope));
+ const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope);
const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
const 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_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-secret-edit-block" },
- (0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
+ (0, import_react7.createElement)(
"div",
{ className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) },
- [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text]) => (0, import_react6.createElement)(
+ [["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_react6.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
+ (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
text
))
),
- mode === "some" ? (0, import_react6.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react6.createElement)("button", {
+ mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.createElement)("button", {
key: bot.id,
type: "button",
className: "bt-secret-bot",
"aria-pressed": picked.includes(bot.id),
onClick: () => toggle(bot.id)
- }, (0, import_react6.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react6.createElement)(CheckIcon) : null))) : null,
- (0, import_react6.createElement)(
+ }, (0, import_react7.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react7.createElement)(CheckIcon) : null))) : null,
+ (0, import_react7.createElement)(
"div",
{ className: "bt-secret-row-actions" },
- (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access"))
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access"))
)
);
}
function ValueEditor({ secret, onSave }) {
- const [value, setValue] = (0, import_react6.useState)("");
- const [replaced, setReplaced] = (0, import_react6.useState)(false);
+ const [value, setValue] = (0, import_react7.useState)("");
+ const [replaced, setReplaced] = (0, import_react7.useState)(false);
const long = value.trim().length >= MIN_VALUE;
const save = () => {
if (!long) return;
@@ -5261,66 +5306,66 @@ function ValueEditor({ secret, onSave }) {
setValue(next);
setReplaced(false);
};
- return (0, import_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-secret-edit-block" },
- (0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
- (0, import_react6.createElement)(
+ (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_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
- (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
+ (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
),
- value !== "" && !long ? (0, import_react6.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react6.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react6.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_react6.useState)(false);
- (0, import_react6.useEffect)(() => {
+ const [confirm, setConfirm] = (0, import_react7.useState)(false);
+ (0, import_react7.useEffect)(() => {
if (!open) setConfirm(false);
}, [open]);
const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you");
const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean);
const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · ");
- return (0, import_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-secret-item", "data-open": open ? "" : void 0 },
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react6.createElement)("span", { className: "bt-secret-title" }, (0, import_react6.createElement)(KeyIcon), (0, import_react6.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react6.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
- (0, import_react6.createElement)("span", { className: "bt-set-hint" }, meta)
+ (0, import_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
+ (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta)
),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
"span",
{ className: "bt-set-control" },
- secret.scope === "all" ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react6.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react6.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
+ secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
)
),
- open ? (0, import_react6.createElement)(
+ open ? (0, import_react7.createElement)(
"div",
{ className: "bt-secret-edit" },
- (0, import_react6.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
- (0, import_react6.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
+ (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
+ (0, import_react7.createElement)(
"div",
{ className: "bt-secret-row-actions" },
confirm ? [
- (0, import_react6.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
- (0, import_react6.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
- (0, import_react6.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_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react6.createElement)(TrashIcon), t("Delete"))
+ (0, import_react7.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
+ (0, import_react7.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
+ (0, import_react7.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete"))
+ ] : (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete"))
)
) : null
);
}
function AddSecret({ actions, run, onDone }) {
- const [name, setName] = (0, import_react6.useState)("");
- const [purpose, setPurpose] = (0, import_react6.useState)("");
- const [value, setValue] = (0, import_react6.useState)("");
+ const [name, setName] = (0, import_react7.useState)("");
+ const [purpose, setPurpose] = (0, import_react7.useState)("");
+ const [value, setValue] = (0, import_react7.useState)("");
const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE;
const save = () => {
if (!ready) return;
@@ -5331,43 +5376,43 @@ function AddSecret({ actions, run, onDone }) {
}
});
};
- return (0, import_react6.createElement)(
+ return (0, import_react7.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-secret-add" },
- (0, import_react6.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_react6.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_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }),
+ (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }),
+ (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })),
+ (0, import_react7.createElement)(
"div",
{ className: "bt-secret-row-actions" },
- (0, import_react6.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")),
- (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
- (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key"))
+ (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")),
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
+ (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key"))
)
);
}
function SecretsPage({ roster, actions, run, Section }) {
- const [open, setOpen] = (0, import_react6.useState)(null);
- const [adding, setAdding] = (0, import_react6.useState)(false);
+ const [open, setOpen] = (0, import_react7.useState)(null);
+ const [adding, setAdding] = (0, import_react7.useState)(false);
const secrets = roster.secrets ?? [];
return [
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
"p",
{ key: "about", className: "bt-secret-about" },
t("Keys your Bots asked for. Bots never see the values: they use them as "),
- (0, import_react6.createElement)("code", null, "$DSH_SECRET_NAME"),
+ (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"),
t(" in their shell, and every value is replaced by "),
- (0, import_react6.createElement)("code", null, "[secret:NAME]"),
+ (0, import_react7.createElement)("code", null, "[secret:NAME]"),
t(" before anything goes to a model.")
),
- (0, import_react6.createElement)(
+ (0, import_react7.createElement)(
Section,
{ key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
- secrets.length === 0 && !adding ? (0, import_react6.createElement)("div", { className: "bt-set-row" }, (0, import_react6.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_react6.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
- adding ? (0, import_react6.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
+ secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
+ secrets.map((secret) => (0, import_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
+ adding ? (0, import_react7.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
),
- adding ? null : (0, import_react6.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react6.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 = `
@@ -5494,10 +5539,10 @@ function facePersonLabel(person) {
}
// src/client/held-reply.js
-var import_react7 = require("react");
+var import_react8 = require("react");
var call3 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
var Boundary;
-var boundary = () => Boundary ??= class HeldReplyBoundary extends import_react7.Component {
+var boundary = () => Boundary ??= class HeldReplyBoundary extends import_react8.Component {
constructor(props) {
super(props);
this.state = { failed: false };
@@ -5514,16 +5559,16 @@ var boundary = () => Boundary ??= class HeldReplyBoundary extends import_react7.
};
function useInlineEntry(policy) {
const slots = registry();
- const [, setVersion] = (0, import_react7.useState)(0);
- (0, import_react7.useEffect)(() => slots.subscribe(() => setVersion(slots.version())), [slots]);
+ const [, setVersion] = (0, import_react8.useState)(0);
+ (0, import_react8.useEffect)(() => slots.subscribe(() => setVersion(slots.version())), [slots]);
return slots.entries("flybot.approvals").find((entry) => entry.inline === true && entry.policy === policy);
}
function HeldReplyActions({ held, actions }) {
const custom = useInlineEntry(held?.by);
- const [busy, setBusy] = (0, import_react7.useState)(false);
- const [error, setError] = (0, import_react7.useState)("");
+ const [busy, setBusy] = (0, import_react8.useState)(false);
+ const [error, setError] = (0, import_react8.useState)("");
const waiting = isApprovalWaiting(held?.status);
- (0, import_react7.useEffect)(() => {
+ (0, import_react8.useEffect)(() => {
if (!waiting) return void 0;
claimApprovals(`held-reply:${held.id}`, [held.id]);
return () => claimApprovals(`held-reply:${held.id}`, []);
@@ -5542,33 +5587,33 @@ function HeldReplyActions({ held, actions }) {
};
if (!waiting) {
const failed = held.status === "failed";
- return (0, import_react7.createElement)(
+ return (0, import_react8.createElement)(
"div",
{ className: "bt-held-note", "data-state": held.status, title: failed && held.error ? held.error : void 0 },
- (0, import_react7.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")),
+ (0, import_react8.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")),
// #968: the terminal answer comes from one predicate, not from `status === 'failed'` here and a
// different field elsewhere. A failure that can never succeed draws no button on any surface.
- failed && canRetry(held) ? (0, import_react7.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
- error ? (0, import_react7.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
+ failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
+ error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
);
}
- const plain = (0, import_react7.createElement)(
+ const plain = (0, import_react8.createElement)(
"div",
{ className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") },
- (0, import_react7.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")),
- (0, import_react7.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
- (0, import_react7.createElement)(
+ (0, import_react8.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")),
+ (0, import_react8.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
+ (0, import_react8.createElement)(
"div",
{ className: "bt-flybot-approval-actions" },
- (0, import_react7.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")),
- (0, import_react7.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve"))
+ (0, import_react8.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")),
+ (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve"))
),
- error ? (0, import_react7.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
+ error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
);
- return (0, import_react7.createElement)(
+ return (0, import_react8.createElement)(
"div",
{ className: "bt-held-actions", "data-flybot": "held-reply-actions" },
- custom ? (0, import_react7.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react7.createElement)(custom.component, { held, decide, call: call3, actions })) : plain
+ custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain
);
}
@@ -5583,7 +5628,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_react8 = require("react");
+var import_react9 = require("react");
var import_react_dom = require("react-dom");
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") });
@@ -5596,7 +5641,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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
texts2.map((text, index) => {
@@ -5604,21 +5649,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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ key: index, className: "bt-msg" },
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"div",
{ className: "bt-msg-line" },
lead && last ? lead : null,
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"div",
{ className: "bt-bubble" },
- (0, import_react8.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions })
+ (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions })
),
- live ? null : (0, import_react8.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_react8.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react8.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",
@@ -5644,7 +5689,7 @@ function leaveNoticeCoveredByClose(room2, text) {
return false;
}
function useDismiss(open, close, refs) {
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (refs.some((ref) => ref.current?.contains(event.target))) return;
@@ -5665,11 +5710,11 @@ function useDismiss(open, close, refs) {
}, [open]);
}
function MessageTools({ text, name, sessionId, react, actions }) {
- const [panel, setPanel] = (0, import_react8.useState)(null);
- const [place, setPlace] = (0, import_react8.useState)(null);
- const [copied, setCopied] = (0, import_react8.useState)(false);
- const tools = (0, import_react8.useRef)(null);
- const pop = (0, import_react8.useRef)(null);
+ 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) => {
@@ -5694,25 +5739,25 @@ function MessageTools({ text, name, sessionId, react, actions }) {
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1e3);
};
- const item = (label, Icon, run) => (0, import_react8.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
+ const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
close();
run();
- } }, (0, import_react8.createElement)(Icon), label);
- return (0, import_react8.createElement)(
+ } }, (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_react8.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_react8.createElement)(SmileIcon)) : null,
- sessionId ? (0, import_react8.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react8.createElement)(ReplyIcon)) : null,
- (0, import_react8.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_react8.createElement)(CheckIcon) : (0, import_react8.createElement)(DotsIcon)),
- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react8.createElement)(
+ react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null,
+ sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, 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_react8.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => {
+ REACTIONS.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => {
close();
react(emoji);
} }, emoji))
), document.body) : null,
- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react8.createElement)(
+ 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),
@@ -5722,11 +5767,11 @@ function MessageTools({ text, name, sessionId, react, actions }) {
}
function ThinkingRow({ text }) {
if (typeof text !== "string" || text.trim() === "") return null;
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"details",
{ className: "bt-think" },
- (0, import_react8.createElement)("summary", null, (0, import_react8.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), t("Thought it through")),
- (0, import_react8.createElement)("div", { className: "bt-think-text" }, text)
+ (0, import_react9.createElement)("summary", null, (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), t("Thought it through")),
+ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text)
);
}
function useFileMentions({ node, openFile, fileMentions, useTurnData }) {
@@ -5734,7 +5779,7 @@ 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_react8.useMemo)(() => {
+ return (0, import_react9.useMemo)(() => {
if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0;
return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0;
}, [fileMentions, openFile, seq, tail, turn]);
@@ -5748,7 +5793,7 @@ function useClosingText(node, useTurnData) {
function friendLead(bot, texts2) {
if (bot?.remote === void 0 || texts2.length === 0) return void 0;
if (texts2.every((text) => /^Not sent:/.test(text.trim()))) return void 0;
- return (0, import_react8.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react8.createElement)(BotMark, { bot, size: 22 }));
+ 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);
@@ -5768,14 +5813,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;
- const bubbles = texts2.length ? person ? (0, import_react8.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react8.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_react8.createElement)(
- import_react8.Fragment,
+ const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null;
+ return (0, import_react9.createElement)(
+ import_react9.Fragment,
null,
relays.map((relay) => {
const source = relay.source;
const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
- return (0, import_react8.createElement)(EventLine, {
+ return (0, import_react9.createElement)(EventLine, {
key: relay.seq,
prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"),
bots: [sender],
@@ -5785,56 +5830,56 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use
whole: true
});
}),
- reasoning ? (0, import_react8.createElement)(ThinkingRow, { text: reasoning }) : null,
- held && bubbles ? (0, import_react8.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
+ reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null,
+ held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
);
}
function HeldReply({ held, roster, sessionId, actions, closing, children }) {
const bot = roster.byId[sessionId];
const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to);
const toName = cleanPeerName(held.toOwner) || (chat ? friendBotOwner(chat) : ownerNameOf({ handle: held.to })) || held.to || t("a friend");
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-held-reply", "data-state": held.status, "data-held": isApprovalWaiting(held.status) ? "" : void 0 },
children,
- closing ? (0, import_react8.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
+ closing ? (0, import_react9.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
);
}
function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) {
- const lead = (0, import_react8.createElement)("span", { className: "bt-lead", title: name }, (0, import_react8.createElement)(PersonMark, { name }));
- return (0, import_react8.createElement)(
+ const lead = (0, import_react9.createElement)("span", { className: "bt-lead", title: name }, (0, import_react9.createElement)(PersonMark, { name }));
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-group-msg bt-person-msg", "data-person": name },
- (0, import_react8.createElement)("span", { className: "bt-group-name" }, name),
- (0, import_react8.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead })
+ (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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-event" },
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"button",
{ type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
- (0, import_react8.createElement)("span", null, prefix),
- (0, import_react8.createElement)(BotMark, { bot: bots[0], size: 16 }),
- (0, import_react8.createElement)("span", { className: "bt-event-name" }, bots[0].name)
+ (0, import_react9.createElement)("span", null, prefix),
+ (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }),
+ (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name)
)
);
}
const parts = [];
- parts.push((0, import_react8.createElement)("span", { key: "p" }, prefix));
+ parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix));
bots.forEach((bot, index) => {
- if (index > 0) parts.push((0, import_react8.createElement)("span", { key: `j${index}` }, joiner));
- parts.push((0, import_react8.createElement)(
+ if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner));
+ parts.push((0, import_react9.createElement)(
"button",
{ key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
- (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react9.createElement)(BotMark, { bot, size: 16 }),
bot.name
));
});
- return (0, import_react8.createElement)("div", { className: "bt-event" }, parts);
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, parts);
}
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();
@@ -5863,30 +5908,30 @@ function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) {
function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, seq }) {
const held = heldLineState(room2, { seq, botId: bot.id, text });
if (held === "rejected") {
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-event bt-not-sent", "data-flybot": "held-not-sent", role: "status" },
- (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
- (0, import_react8.createElement)("span", null, t("{name} · Not sent", { name: bot.name }))
+ (0, import_react9.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name }))
);
}
if (held === "held" || isHeldLine(room2, text, bot)) return null;
- const lead = (0, import_react8.createElement)(
+ 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_react8.createElement)(BotMark, { bot, size: 22 })
+ (0, import_react9.createElement)(BotMark, { bot, size: 22 })
);
- const name = (0, import_react8.createElement)(
+ const name = (0, import_react9.createElement)(
"span",
{ className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
faceBotLabel(room2, roster, bot) ?? bot.name,
- room2?.admin === bot.id ? (0, import_react8.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
+ room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
);
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-group-msg" },
name,
- (0, import_react8.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
+ (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
);
}
function remoteSpeaker(source, roster, sessionId) {
@@ -5928,53 +5973,53 @@ function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi
const files = Array.isArray(source.attachments) ? source.attachments : [];
const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
if (person) {
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle },
// One short header: the person, and the Bot that carried the line only when it is known (#275).
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"span",
{ className: "bt-group-name", title: handle },
person,
- carrier ? (0, import_react8.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null
+ carrier ? (0, import_react9.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null
),
- (0, import_react8.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react8.createElement)("span", { className: "bt-lead", title: person }, (0, import_react8.createElement)(PersonMark, { name: person })) }),
- files.length ? (0, import_react8.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }),
+ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
);
}
- const mark = (0, import_react8.createElement)(BotMark, { bot, size: 22 });
- const lead = chatId ? (0, import_react8.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react8.createElement)("span", { className: "bt-lead", title: handle }, mark);
- return (0, import_react8.createElement)(
+ const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 });
+ const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark);
+ return (0, import_react9.createElement)(
"div",
{ className: "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_react8.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react8.createElement)(
+ 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)) } },
name,
- (0, import_react8.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react8.createElement)(GlobeMark), handle)
+ (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_react8.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }),
- files.length ? (0, import_react8.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }),
+ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
);
}
function FirstContact({ source, roster, sessionId }) {
const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
const who = person || remoteSpeaker(source, roster).name;
const bot = roster.byId[sessionId]?.name ?? t("your Bot");
- return (0, import_react8.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react8.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot })));
+ return (0, import_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot })));
}
function GlobeMark() {
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"svg",
{ width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react8.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
- (0, import_react8.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
+ (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
);
}
function RemoteFiles({ files, plugin, botId }) {
- const [busy, setBusy] = (0, import_react8.useState)("");
- const [error, setError] = (0, import_react8.useState)("");
+ const [busy, setBusy] = (0, import_react9.useState)("");
+ const [error, setError] = (0, import_react9.useState)("");
const download2 = async (file) => {
setBusy(file.id);
setError("");
@@ -5992,23 +6037,23 @@ function RemoteFiles({ files, plugin, botId }) {
setBusy("");
}
};
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-file-chips" },
- files.map((file) => (0, import_react8.createElement)(
+ files.map((file) => (0, import_react9.createElement)(
"button",
{ key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) },
`📎 ${file.name} · ${fileSize(file.size)}`
)),
- error ? (0, import_react8.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
+ 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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-event" },
- (0, import_react8.createElement)("span", null, prefix),
- room2 ? (0, import_react8.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react8.createElement)("span", null, name) : null
+ (0, import_react9.createElement)("span", null, prefix),
+ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null
);
}
function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
@@ -6019,14 +6064,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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-event" },
- (0, import_react8.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react8.createElement)(MemoryIcon), label)
+ (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_react8.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react8.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) {
@@ -6035,11 +6080,11 @@ var scrollParent = (node) => {
return null;
};
function EarlierParts({ sessionId, roster, actions }) {
- const [view, setView] = (0, import_react8.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
- const top = (0, import_react8.useRef)(null);
- const busy = (0, import_react8.useRef)(false);
- const alive = (0, import_react8.useRef)(true);
- const restore = (0, import_react8.useRef)(null);
+ const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
+ const top = (0, import_react9.useRef)(null);
+ const busy = (0, import_react9.useRef)(false);
+ const alive = (0, import_react9.useRef)(true);
+ const restore = (0, import_react9.useRef)(null);
const load = (cursor) => {
if (busy.current) return;
busy.current = true;
@@ -6055,19 +6100,19 @@ function EarlierParts({ sessionId, roster, actions }) {
if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
});
};
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
alive.current = true;
load(void 0);
return () => {
alive.current = false;
};
}, [sessionId]);
- (0, import_react8.useLayoutEffect)(() => {
+ (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_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
const node = top.current;
if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0;
const observer = new IntersectionObserver((entries2) => {
@@ -6083,35 +6128,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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading },
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"div",
{ ref: top, className: "bt-earlier-top" },
- view.loading ? (0, import_react8.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react8.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react8.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null
+ view.loading ? (0, import_react9.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null
),
rows.map((row, index) => {
const key = rows.length - index;
switch (row.kind) {
case "divider":
- return (0, import_react8.createElement)(PartLine, { key, time: row.time });
+ return (0, import_react9.createElement)(PartLine, { key, time: row.time });
case "user":
- return (0, import_react8.createElement)("div", { key, className: "bt-urow" }, (0, import_react8.createElement)(
+ return (0, import_react9.createElement)("div", { key, className: "bt-urow" }, (0, import_react9.createElement)(
"div",
{ className: "bt-ububble" },
- row.text ? (0, import_react8.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null,
- row.images ? (0, import_react8.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null
+ row.text ? (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null,
+ row.images ? (0, import_react9.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null
));
case "answer":
- return (0, import_react8.createElement)(AnsweredCell, { key, node: { data: row } });
+ return (0, import_react9.createElement)(AnsweredCell, { key, node: { data: row } });
case "bot":
- return (0, import_react8.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions });
+ return (0, import_react9.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions });
case "event": {
- if (row.role === "report") return (0, import_react8.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions });
+ if (row.role === "report") return (0, import_react9.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions });
const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" };
const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from");
- return (0, import_react8.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
+ return (0, import_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
if (selfId && other.id) actions.openExchange(selfId, other.id);
}, whole: true });
}
@@ -6119,7 +6164,7 @@ function EarlierParts({ sessionId, roster, actions }) {
return null;
}
}),
- (0, import_react8.createElement)(PartLine, { time: view.startedAt })
+ (0, import_react9.createElement)(PartLine, { time: view.startedAt })
);
}
function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
@@ -6130,7 +6175,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_react8.useLayoutEffect)(() => {
+ (0, import_react9.useLayoutEffect)(() => {
if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
}, [relayed, sessionId, turn, source.senderSessionId]);
const order = useSessions((list) => {
@@ -6149,31 +6194,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_react8.createElement)(import_react8.Fragment, null, (0, import_react8.createElement)("div", { className: "bt-open-new" }, (0, import_react8.createElement)(NewDivider), (0, import_react8.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
+ return unreadOpening ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
function triggerBody() {
if (source.form === "remote-chat") return null;
if (source.shown === true) return null;
if (source.remote?.handle) {
- const message = (0, import_react8.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_react8.createElement)(import_react8.Fragment, null, (0, import_react8.createElement)(FirstContact, { source, roster, sessionId }), message) : message;
+ 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_react8.createElement)(EarlierParts, { sessionId, roster, actions });
+ 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_react8.createElement)("div", { className: "bt-event" }, (0, import_react8.createElement)("span", null, contentText(data.content)));
+ 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.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_react8.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
+ return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
}
if (source.role === "report") {
- return (0, import_react8.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
+ return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
}
- if (source.role === "resume" || source.resumed === true) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
- if (source.role === "retry" || source.retried === true) return (0, import_react8.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)])),
@@ -6187,7 +6232,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_react8.createElement)(EventLine, {
+ return (0, import_react9.createElement)(EventLine, {
prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"),
bots,
onOpen: (bot) => actions.openExchange(selfId, bot.id),
@@ -6196,24 +6241,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us
}
if (source.kind === "schedule") {
const prompts = reminderPrompts(contentText(data.content));
- return (0, import_react8.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
}
if (source.kind === "user-question-reply") return null;
- return (0, import_react8.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
}
}
function NewDivider() {
- return (0, import_react8.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react8.createElement)("span", null, t("New")));
+ return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New")));
}
function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
if (!open) turnProcess.setOpen(true);
}, [open, turnProcess]);
const turn = node?.location?.turn?.turn ?? node?.data?.turn;
const start = node?.location?.turn?.start?.time;
const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn));
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
actions.noteTurnTime(sessionId, turn, "first", start);
}, [sessionId, turn, start]);
const visit = useUi((state) => state.visits?.[sessionId]);
@@ -6221,7 +6266,7 @@ 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_react8.createElement)(NewDivider) : null;
+ return fresh ? (0, import_react9.createElement)(NewDivider) : null;
}
function Nothing() {
return null;
@@ -6276,7 +6321,7 @@ function ContextCell({ node, useRoster }) {
else if (block?.type === "tool-addition" && block.toolName) parts.push(`Tool added: ${block.toolName}`);
}
if (parts.length === 0) return null;
- return (0, import_react8.createElement)("div", { className: "bt-event" }, parts.join(" · "));
+ return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · "));
}
var SEPARATOR_GAP_MS = 30 * 60 * 1e3;
function clockLabel(time, now = Date.now()) {
@@ -6319,7 +6364,7 @@ var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < vi
function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
const turn = location2.turn;
const time = location2.end.time;
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
actions.noteTurnTime(sessionId, turn, "end", time);
}, [sessionId, turn, time]);
const label = useTurnClock((state) => {
@@ -6333,15 +6378,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_react8.createElement)(
- import_react8.Fragment,
+ return (0, import_react9.createElement)(
+ import_react9.Fragment,
null,
- label ? (0, import_react8.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
- fresh ? (0, import_react8.createElement)(NewDivider) : null
+ label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
+ fresh ? (0, import_react9.createElement)(NewDivider) : null
);
}
function Shimmer({ children }) {
- return (0, import_react8.createElement)("span", { className: "bt-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"],
@@ -6410,15 +6455,15 @@ function workText(tool, step) {
}
function useLiveNote(actions, sessionId, id, entry) {
const key = entry ? JSON.stringify(entry) : "";
- (0, import_react8.useEffect)(() => {
+ (0, import_react9.useEffect)(() => {
if (!key) return void 0;
actions.noteLive(sessionId, id, JSON.parse(key));
return () => actions.noteLive(sessionId, id, null);
}, [sessionId, id, key]);
}
function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) {
- const [live, setLive] = (0, import_react8.useState)({ replies: [] });
- (0, import_react8.useEffect)(() => {
+ const [live, setLive] = (0, import_react9.useState)({ replies: [] });
+ (0, import_react9.useEffect)(() => {
if (done) return void 0;
let stop = false;
let timer;
@@ -6450,7 +6495,7 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
}
if (replies.length === 0) return null;
const room2 = roster.roomsById[sessionId];
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ className: "bt-group-replies" },
replies.map((reply, index) => {
@@ -6460,66 +6505,66 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
if (reply.kind === "user") {
const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId;
if (reply.other) {
- return (0, import_react8.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)(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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who },
- who ? (0, import_react8.createElement)("span", { className: "bt-uauthor" }, who) : null,
- (0, import_react8.createElement)(
+ who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null,
+ (0, import_react9.createElement)(
"div",
{ className: "bt-ububble" },
- (0, import_react8.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" })
+ (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" })
),
- room2?.origin === "face-to-face" ? (0, import_react8.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react8.createElement)(PersonMark, { name: who || void 0 })) : null
+ room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null
);
}
if (reply.kind === "failed") {
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"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_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ key: `${reply.botId}-${index}`, className: "bt-event" },
reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name })
);
}
if (reply.kind === "missed") {
- return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
+ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
}
if (reply.kind === "resumed") {
- return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name }));
+ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name }));
}
if (reply.kind === "cut") {
- return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name }));
+ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name }));
}
if (reply.kind === "approval" || reply.kind === "working") {
- return (0, import_react8.createElement)(
+ return (0, import_react9.createElement)(
"div",
{ key: `${reply.botId}-${index}`, className: "bt-event" },
- (0, import_react8.createElement)(
+ (0, import_react9.createElement)(
"button",
{ type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) },
- (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
- (0, import_react8.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name }))
+ (0, import_react9.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react9.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name }))
)
);
}
if (reply.kind === "stopped") {
- return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name }));
+ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name }));
}
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") {
- return (0, import_react8.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text));
+ return (0, import_react9.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text));
}
const remote = reply.remote || reply.handle && !roster.byId[reply.botId];
if (remote) {
- return (0, import_react8.createElement)(RemoteMessage, {
+ return (0, import_react9.createElement)(RemoteMessage, {
key: `${reply.botId || reply.handle}-${index}`,
source: { remote: { handle: reply.handle || reply.botId }, senderName: reply.name || reply.who, senderSessionId: reply.botId },
text: remoteMessageText(reply.text),
@@ -6530,7 +6575,7 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
useUi
});
}
- return (0, import_react8.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 });
+ 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 });
})
);
}
@@ -6551,7 +6596,7 @@ function callTree(root) {
}
function useShownCards(actions, sessionId, callIds) {
const key = JSON.stringify(callIds);
- (0, import_react8.useLayoutEffect)(() => {
+ (0, import_react9.useLayoutEffect)(() => {
const ids = JSON.parse(key);
if (ids.length === 0 || !actions.noteCards) return void 0;
actions.noteCards(sessionId, ids, 1);
@@ -6566,25 +6611,25 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
switch (tool) {
case "message_bot": {
const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
- if (done && call4.isError) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
- return (0, import_react8.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
+ return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "stop_bot": {
const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
- if (done && call4.isError) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
- return (0, import_react8.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
+ return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "create_bot": {
const created = findBot(arg("name"));
if (!done || created === void 0) return null;
- return (0, import_react8.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
+ return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
}
case "update_bot": {
const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
- return done && target ? (0, import_react8.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
+ return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
}
case "group_relay":
- return (0, import_react8.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
+ 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":
@@ -6598,18 +6643,18 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
if (!done) return null;
const result = contentText(call4.content);
const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
- if (call4.isError || !named) return (0, import_react8.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
+ if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
const name = named[2];
const room2 = roster.rooms.find((entry) => entry.name === name);
- return (0, import_react8.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
+ return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
}
case "ask_user": {
const question = roster.questions?.[sessionId];
- if (!question || question.callId !== call4.callId) return (0, import_react8.createElement)("span", { className: "bt-q-gone", hidden: true });
- return (0, import_react8.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
+ if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
+ return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
}
case "request_secret":
- return (0, import_react8.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions });
+ return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions });
case "ask_user_question": {
if (!done) return null;
let parsed = null;
@@ -6621,16 +6666,16 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
const answer = parsed?.answers?.[0];
const question = argValue("questions")?.[0];
if (!answer) return null;
- return (0, import_react8.createElement)("div", { className: "bt-q-inline" }, (0, import_react8.createElement)(SettledQuestion, { question, answer }));
+ return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer }));
}
case "read_group_chat": {
if (!done) return null;
const room2 = roster.rooms.find((entry) => entry.name === arg("group"));
- return room2 ? (0, import_react8.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null;
+ return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null;
}
case "remember":
case "forget":
- return done && !call4.isError ? (0, import_react8.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
+ return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
default:
return null;
}
@@ -6649,7 +6694,7 @@ function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
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, findBot };
- return (0, import_react8.createElement)(import_react8.Fragment, null, drawn.map((call4, index) => (0, import_react8.createElement)(import_react8.Fragment, { key: call4.callId ?? index }, renderCall(call4, env))));
+ 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))));
}
// src/client/awaiting.js
@@ -6669,7 +6714,7 @@ function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
}
// src/client/activity-row.js
-var import_react9 = require("react");
+var import_react10 = require("react");
var import_react_dom2 = require("react-dom");
var PHRASE_HOLD_MS = 800;
var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]);
@@ -6684,9 +6729,9 @@ var TAIL_HOST = "tail";
var REMOTE_GRACE_MS = 1500;
function useAwaitingReply(roster, sessionId) {
const at = roster?.remoteAwaiting?.[sessionId];
- const [, tick] = (0, import_react9.useState)(0);
+ const [, tick] = (0, import_react10.useState)(0);
const shown = awaitingReply(roster, sessionId);
- (0, import_react9.useEffect)(() => {
+ (0, import_react10.useEffect)(() => {
if (!shown) return void 0;
const timer = setTimeout(() => tick((n) => n + 1), Math.max(0, at + AWAIT_REPLY_MS - Date.now()) + 50);
return () => clearTimeout(timer);
@@ -6708,10 +6753,10 @@ var lastTurnOnScreen = (kind) => {
return top;
};
function useReplyAfterSend(remote, sending) {
- const sendTurn = (0, import_react9.useRef)(-1);
- const wasSending = (0, import_react9.useRef)(false);
- const [replied, setReplied] = (0, import_react9.useState)(false);
- (0, import_react9.useEffect)(() => {
+ const sendTurn = (0, import_react10.useRef)(-1);
+ const wasSending = (0, import_react10.useRef)(false);
+ const [replied, setReplied] = (0, import_react10.useState)(false);
+ (0, import_react10.useEffect)(() => {
if (!remote) return void 0;
if (sending) {
wasSending.current = true;
@@ -6742,8 +6787,8 @@ function useReplyAfterSend(remote, sending) {
return replied;
}
function useRunningHost(active, where = RUNNING_HOST) {
- const [host, setHost] = (0, import_react9.useState)(null);
- (0, import_react9.useEffect)(() => {
+ const [host, setHost] = (0, import_react10.useState)(null);
+ (0, import_react10.useEffect)(() => {
if (!active) {
setHost(null);
return void 0;
@@ -6767,9 +6812,9 @@ function useRunningHost(active, where = RUNNING_HOST) {
return host;
}
function useRowEntrances(replacing) {
- const check = (0, import_react9.useRef)(replacing);
+ const check = (0, import_react10.useRef)(replacing);
check.current = replacing;
- (0, import_react9.useEffect)(() => {
+ (0, import_react10.useEffect)(() => {
const seen = /* @__PURE__ */ new WeakSet();
const started = Date.now();
let column = null;
@@ -6820,31 +6865,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_react9.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
+ const entries2 = (0, import_react10.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
const streaming = entries2.some((entry) => entry.text);
const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at);
const relaying = tools.some((entry) => entry.relay);
const awaiting = useAwaitingReply(roster, sessionId);
- const sendRun = (0, import_react9.useRef)(false);
+ const sendRun = (0, import_react10.useRef)(false);
if (!running) sendRun.current = false;
else if (pendingInput > 0) sendRun.current = true;
const replied = useReplyAfterSend(bot?.remote !== void 0, running && sendRun.current);
const visible = bot?.remote !== void 0 ? !streaming && (running ? sendRun.current : awaiting && !replied) : running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
- const lastRun = (0, import_react9.useRef)({ running, send: false });
+ const lastRun = (0, import_react10.useRef)({ running, send: false });
const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running;
lastRun.current = { running, send: sendRun.current };
const steady = bridging;
const tool = tools.at(-1);
- const inputRef = (0, import_react9.useRef)(0);
+ const inputRef = (0, import_react10.useRef)(0);
const inputAt = Math.max(inputRef.current, pendingInput);
inputRef.current = inputAt;
const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null;
const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
- const [shown, setShown] = (0, import_react9.useState)(null);
+ const [shown, setShown] = (0, import_react10.useState)(null);
const wantKey = want?.key ?? null;
- const markState = (0, import_react9.useRef)("thinking");
+ const markState = (0, import_react10.useRef)("thinking");
if (want) markState.current = want.state;
- (0, import_react9.useEffect)(() => {
+ (0, import_react10.useEffect)(() => {
if (wantKey === null || shown?.key === wantKey) return void 0;
const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() });
const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0;
@@ -6855,12 +6900,12 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us
const timer = setTimeout(make, wait);
return () => clearTimeout(timer);
}, [wantKey, shown]);
- const [phase, setPhase] = (0, import_react9.useState)(null);
- const endedAt = (0, import_react9.useRef)(0);
- const shift = (0, import_react9.useRef)(null);
- const leaving = (0, import_react9.useRef)(false);
+ 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_react9.useEffect)(() => {
+ (0, import_react10.useEffect)(() => {
if (visible) {
setPhase("in");
return void 0;
@@ -6877,7 +6922,7 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us
else leave();
return () => clearTimeout(timer);
}, [visible]);
- (0, import_react9.useLayoutEffect)(() => {
+ (0, import_react10.useLayoutEffect)(() => {
if (phase !== null) return;
endedAt.current = Date.now();
setShown(null);
@@ -6888,37 +6933,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_react9.useEffect)(() => {
+ (0, import_react10.useEffect)(() => {
if (!running) actions.clearLive(sessionId);
}, [running, sessionId]);
useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
- const [now, setNow] = (0, import_react9.useState)(Date.now);
- (0, import_react9.useEffect)(() => {
+ const [now, setNow] = (0, import_react10.useState)(Date.now);
+ (0, import_react10.useEffect)(() => {
if (phase !== "in") return void 0;
const timer = setInterval(() => setNow(Date.now()), 1e4);
return () => clearInterval(timer);
}, [phase]);
const remote = bot?.remote !== void 0;
const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST);
- const drawn = (0, import_react9.useRef)(false);
+ const drawn = (0, import_react10.useRef)(false);
const carry = drawn.current;
- (0, import_react9.useEffect)(() => {
+ (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_react9.createElement)(
+ return (0, import_react_dom2.createPortal)((0, import_react10.createElement)(
"div",
{ className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true },
- bot ? (0, import_react9.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
- (0, import_react9.createElement)(
+ 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_react9.createElement)(
+ (0, import_react10.createElement)(
"span",
{ key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
- (0, import_react9.createElement)(Shimmer, null, `${shown.text}…`),
- elapsed ? (0, import_react9.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
+ (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`),
+ elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
)
)
), host);
@@ -6935,15 +6980,15 @@ function showsDshRow(dsh, app) {
}
// src/client/overlay-hooks.js
-var import_react10 = require("react");
+var import_react11 = require("react");
var escapeLayers = [];
var onEscapeKey = (event) => {
if (event.key === "Escape") escapeLayers.at(-1)?.current();
};
function useEscape(onClose) {
- const ref = (0, import_react10.useRef)(onClose);
+ const ref = (0, import_react11.useRef)(onClose);
ref.current = onClose;
- (0, import_react10.useEffect)(() => {
+ (0, import_react11.useEffect)(() => {
if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey);
escapeLayers.push(ref);
return () => {
@@ -6953,8 +6998,8 @@ function useEscape(onClose) {
}, []);
}
function usePaneLeft() {
- const [paneLeft, setPaneLeft] = (0, import_react10.useState)(280);
- (0, import_react10.useLayoutEffect)(() => {
+ const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280);
+ (0, import_react11.useLayoutEffect)(() => {
const column = document.querySelector('[class*="_footArea"]')?.parentElement;
if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right));
}, []);
@@ -6962,7 +7007,7 @@ function usePaneLeft() {
}
// src/client/member-status.js
-var import_react11 = require("react");
+var import_react12 = require("react");
var NO_WORK = { bots: {}, rooms: {}, approvals: [] };
var ROWS_MAX = 4;
var stepIn = (work, botId, roomId, tool) => {
@@ -7009,39 +7054,39 @@ function summaryText(busy) {
return { waiting, doing };
}
function Face({ entry, size }) {
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"span",
{ className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 },
- (0, import_react11.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
+ (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
);
}
var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : "";
function WorkRow({ bot, tool, step, label, at, now, onOpen }) {
const text = `${label} · ${workText(tool, step)}`;
const elapsed = elapsedSince(now, at);
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"button",
{ type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
- (0, import_react11.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }),
- (0, import_react11.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react11.createElement)(Shimmer, null, text)),
- elapsed ? (0, import_react11.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
+ (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 SummaryLine({ busy, now, onOpen }) {
const { waiting, doing } = summaryText(busy);
const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : "";
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"button",
{ type: "button", className: "bt-mw-row bt-mw-line", "aria-label": [waiting, doing, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
- (0, import_react11.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react11.createElement)(Face, { key: entry.id, entry, size: 16 }))),
- (0, import_react11.createElement)(
+ (0, import_react12.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react12.createElement)(Face, { key: entry.id, entry, size: 16 }))),
+ (0, import_react12.createElement)(
"span",
{ className: "bt-mw-row-text" },
- waiting ? (0, import_react11.createElement)("span", { className: "bt-mw-ask" }, waiting) : null,
+ waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null,
waiting && doing ? " · " : null,
- doing ? (0, import_react11.createElement)(Shimmer, null, doing) : null
+ doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null
),
- elapsed ? (0, import_react11.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
+ elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
);
}
function MemberDock({ sessionId, useRoster, useWork, actions }) {
@@ -7054,20 +7099,20 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) {
const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : [];
const shown = busy.length > 0 || groups.length > 0;
const now = useNow(shown);
- const ref = (0, import_react11.useRef)(null);
+ const ref = (0, import_react12.useRef)(null);
useFlowColumn(ref, shown, sessionId);
if (!shown) return null;
if (room2) {
const { rows, rest } = splitBusy(busy);
const open = () => actions.showDetails(sessionId);
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"div",
{ ref, className: "bt-mw-dock-stack" },
- rows.map((entry) => (0, import_react11.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })),
- rest.length > 0 ? (0, import_react11.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null
+ rows.map((entry) => (0, import_react12.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })),
+ rest.length > 0 ? (0, import_react12.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null
);
}
- return (0, import_react11.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react11.createElement)(WorkRow, {
+ return (0, import_react12.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react12.createElement)(WorkRow, {
key: item.room.id,
bot,
tool: item.tool,
@@ -7079,7 +7124,7 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) {
})));
}
function useFlowColumn(ref, active, key) {
- (0, import_react11.useLayoutEffect)(() => {
+ (0, import_react12.useLayoutEffect)(() => {
const node = ref.current;
const flow = document.querySelector("[data-chat-flow]");
if (!active || !node || !flow) return void 0;
@@ -7096,8 +7141,8 @@ function useFlowColumn(ref, active, key) {
}, [active, key]);
}
function useNow(active) {
- const [now, setNow] = (0, import_react11.useState)(Date.now);
- (0, import_react11.useEffect)(() => {
+ const [now, setNow] = (0, import_react12.useState)(Date.now);
+ (0, import_react12.useEffect)(() => {
if (!active) return void 0;
const timer = setInterval(() => setNow(Date.now()), 3e4);
return () => clearInterval(timer);
@@ -7110,39 +7155,39 @@ var alwaysChoices = (approval, bot) => bot?.remote ? [] : [
...approval.roomId ? [["always-room", "Always allow for this group"]] : []
];
function ApprovalCard({ approval, bot, actions }) {
- const [busy, setBusy] = (0, import_react11.useState)(false);
+ const [busy, setBusy] = (0, import_react12.useState)(false);
const answer = (choice) => {
setBusy(true);
void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false));
};
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"div",
{ className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) },
- (0, import_react11.createElement)("div", { className: "bt-mw-tool" }, approval.tool),
- approval.detail?.text ? (0, import_react11.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null,
- approval.reason ? (0, import_react11.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null,
- approval.detail?.code ? (0, import_react11.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null,
- approval.answerable ? (0, import_react11.createElement)(
+ (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",
{ className: "bt-mw-actions" },
- (0, import_react11.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")),
- (0, import_react11.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")),
- ...alwaysChoices(approval, bot).map(([choice, label]) => (0, import_react11.createElement)("button", { key: choice, type: "button", className: "bt-q-btn", "data-choice": choice, disabled: busy, onClick: () => answer(choice) }, t(label)))
- ) : (0, import_react11.createElement)(
+ (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",
{ className: "bt-mw-actions" },
- (0, import_react11.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name }))
+ (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 MemberWork({ entry, bot, room: room2, actions, now }) {
- const [stopping, setStopping] = (0, import_react11.useState)(false);
+ const [stopping, setStopping] = (0, import_react12.useState)(false);
if (entry.state === "ask") {
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"div",
{ className: "bt-mw-member" },
- (0, import_react11.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`),
- entry.approvals.map((approval) => (0, import_react11.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions }))
+ (0, import_react12.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`),
+ entry.approvals.map((approval) => (0, import_react12.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions }))
);
}
if (entry.state !== "work") return null;
@@ -7150,14 +7195,14 @@ function MemberWork({ entry, bot, room: room2, actions, now }) {
setStopping(true);
void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false));
};
- return (0, import_react11.createElement)(
+ return (0, import_react12.createElement)(
"div",
{ className: "bt-mw-member" },
- (0, import_react11.createElement)(
+ (0, import_react12.createElement)(
"div",
{ className: "bt-mw-state bt-mw-work" },
- (0, import_react11.createElement)("span", null, (0, import_react11.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)),
- entry.here && entry.doing.handed !== true ? (0, import_react11.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null
+ (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
)
);
}
@@ -7201,14 +7246,14 @@ body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a}
`;
// src/client/inbox.js
-var import_react12 = require("react");
+var import_react13 = require("react");
var KEEP_MS = 7 * 864e5;
var POLL_MS2 = 3e4;
var DONE_MAX = 20;
var FEEDBACK_MS = 5e3;
-var line = (d) => (0, import_react12.createElement)("path", { d });
+var line = (d) => (0, import_react13.createElement)("path", { d });
var icon = (...paths) => function StatusIcon() {
- return (0, import_react12.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, paths.map(line));
+ return (0, import_react13.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, paths.map(line));
};
var RING = "M10 3.25a6.75 6.75 0 1 1 0 13.5 6.75 6.75 0 0 1 0-13.5z";
var InboxIcon = icon("M3.5 11.5l1.9-6.1A1.5 1.5 0 0 1 6.83 4.35h6.34a1.5 1.5 0 0 1 1.43 1.05l1.9 6.1", "M3.5 11.5v3.25a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5V11.5", "M3.5 11.5h3.75l1 1.75h3.5l1-1.75h3.75");
@@ -7243,8 +7288,8 @@ function publishTasks(list) {
for (const listener of taskListeners) listener(sharedTasks);
}
function useTasks(actions) {
- const [tasks, setTasks] = (0, import_react12.useState)(sharedTasks);
- (0, import_react12.useEffect)(() => {
+ const [tasks, setTasks] = (0, import_react13.useState)(sharedTasks);
+ (0, import_react13.useEffect)(() => {
taskListeners.add(setTasks);
let stopped = false;
let timer;
@@ -7455,7 +7500,7 @@ function InboxRow({ wide, useRoster, useWork, useUi, actions }) {
const tasks = useTasks(actions);
const count = needsCount(roster, work, tasks);
const label = count > 0 ? t("Inbox, {count} need you", { count }) : t("Inbox");
- return (0, import_react12.createElement)(
+ return (0, import_react13.createElement)(
"button",
{
type: "button",
@@ -7466,14 +7511,14 @@ function InboxRow({ wide, useRoster, useWork, useUi, actions }) {
"aria-expanded": open,
onClick: () => actions.openInbox()
},
- (0, import_react12.createElement)("span", { className: "bt-inbox-mark" }, (0, import_react12.createElement)(InboxIcon)),
- wide ? (0, import_react12.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null,
- count > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null
+ (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
);
}
function Status({ status }) {
const { label, tone, Icon } = STATUSES[status];
- return (0, import_react12.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react12.createElement)(Icon), (0, import_react12.createElement)("span", null, t(label)));
+ return (0, import_react13.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", null, t(label)));
}
function whenText(card, now) {
if (card.status === "running" || card.status === "task") {
@@ -7485,39 +7530,39 @@ function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) {
const who = card.bot ? card.bot.name : card.room?.name ?? "";
const meta = [who, card.place, whenText(card, now)].filter(Boolean).join(" · ");
const answerable = card.kind === "approval" && card.approval?.answerable === true;
- return (0, import_react12.createElement)(
+ return (0, import_react13.createElement)(
"div",
{ className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status },
- (0, import_react12.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react12.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react12.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null),
- (0, import_react12.createElement)(
+ (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",
{ className: "bt-inbox-copy" },
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)(
"span",
{ className: "bt-inbox-top" },
- (0, import_react12.createElement)(Status, { status: card.status }),
- (0, import_react12.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta)
+ (0, import_react13.createElement)(Status, { status: card.status }),
+ (0, import_react13.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta)
),
- (0, import_react12.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title),
- card.calls > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null,
- card.reason ? (0, import_react12.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, card.reason) : null,
- card.activity ? (0, import_react12.createElement)("span", { className: "bt-inbox-activity", title: card.activity }, t("Latest: {text}", { text: card.activity })) : null,
- card.hint ? (0, import_react12.createElement)("span", { className: "bt-inbox-hint" }, card.hint) : null,
- error ? (0, import_react12.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null
+ (0, import_react13.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title),
+ card.calls > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null,
+ card.reason ? (0, import_react13.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, card.reason) : null,
+ card.activity ? (0, import_react13.createElement)("span", { className: "bt-inbox-activity", title: card.activity }, t("Latest: {text}", { text: card.activity })) : null,
+ card.hint ? (0, import_react13.createElement)("span", { className: "bt-inbox-hint" }, card.hint) : null,
+ error ? (0, import_react13.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null
),
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)(
"span",
{ className: "bt-inbox-actions" },
- answerable ? (0, import_react12.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "reject") }, t("Reject")) : null,
- answerable ? (0, import_react12.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "allow") }, t("Allow once")) : null,
- ...(answerable ? alwaysChoices(card.approval, card.bot) : []).map(([choice, label]) => (0, import_react12.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_react12.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"))
+ answerable ? (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "reject") }, t("Reject")) : null,
+ answerable ? (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "allow") }, t("Allow once")) : null,
+ ...(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 useNow2(ms) {
- const [now, setNow] = (0, import_react12.useState)(Date.now);
- (0, import_react12.useEffect)(() => {
+ const [now, setNow] = (0, import_react13.useState)(Date.now);
+ (0, import_react13.useEffect)(() => {
const timer = setInterval(() => setNow(Date.now()), ms);
return () => clearInterval(timer);
}, [ms]);
@@ -7528,13 +7573,13 @@ function InboxDialog({ roster, useWork, useSessions, actions, leaving = false })
const sessions = useSessions((value) => value);
const tasks = useTasks(actions);
const now = useNow2(3e4);
- const [answered, setAnswered] = (0, import_react12.useState)({});
- const [busy, setBusy] = (0, import_react12.useState)("");
- const [errors, setErrors] = (0, import_react12.useState)({});
- const [feedback, setFeedback] = (0, import_react12.useState)("");
- const close = (0, import_react12.useCallback)(() => actions.closeInbox(), []);
+ const [answered, setAnswered] = (0, import_react13.useState)({});
+ const [busy, setBusy] = (0, import_react13.useState)("");
+ const [errors, setErrors] = (0, import_react13.useState)({});
+ const [feedback, setFeedback] = (0, import_react13.useState)("");
+ const close = (0, import_react13.useCallback)(() => actions.closeInbox(), []);
useEscape(close);
- (0, import_react12.useEffect)(() => {
+ (0, import_react13.useEffect)(() => {
if (!feedback) return void 0;
const timer = setTimeout(() => setFeedback(""), FEEDBACK_MS);
return () => clearTimeout(timer);
@@ -7554,35 +7599,35 @@ function InboxDialog({ roster, useWork, useSessions, actions, leaving = false })
actions.closeInbox();
actions.openSession(card.chat);
};
- return (0, import_react12.createElement)(
+ return (0, import_react13.createElement)(
"div",
{ className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 },
- (0, import_react12.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
+ (0, import_react13.createElement)(
"div",
{ className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" },
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)(
"div",
{ className: "bt-mem-head" },
- (0, import_react12.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")),
- (0, import_react12.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")),
- (0, import_react12.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react12.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close inbox"), autoFocus: true, onClick: close }, (0, import_react12.createElement)(CloseIcon))
+ (0, import_react13.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")),
+ (0, import_react13.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")),
+ (0, import_react13.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react13.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close inbox"), autoFocus: true, onClick: close }, (0, import_react13.createElement)(CloseIcon))
),
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)(
"div",
{ className: "bt-mem-body bt-inbox-body" },
- (0, import_react12.createElement)("div", { className: "bt-inbox-feedback", role: "status", "aria-live": "polite" }, feedback),
- GROUPS.map((group) => (0, import_react12.createElement)(
+ (0, import_react13.createElement)("div", { className: "bt-inbox-feedback", role: "status", "aria-live": "polite" }, feedback),
+ GROUPS.map((group) => (0, import_react13.createElement)(
"section",
{ key: group.id, className: "bt-inbox-group", "aria-labelledby": `bt-inbox-${group.id}` },
- (0, import_react12.createElement)(
+ (0, import_react13.createElement)(
"h3",
{ id: `bt-inbox-${group.id}`, className: "bt-inbox-head" },
- (0, import_react12.createElement)("span", null, t(group.title)),
- groups[group.id].length > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-head-count" }, String(groups[group.id].length)) : null
+ (0, import_react13.createElement)("span", null, t(group.title)),
+ groups[group.id].length > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-head-count" }, String(groups[group.id].length)) : null
),
- groups[group.id].length === 0 ? (0, import_react12.createElement)("p", { className: "bt-inbox-empty" }, t(group.empty)) : (0, import_react12.createElement)("div", { className: "bt-inbox-list" }, groups[group.id].map((card) => (0, import_react12.createElement)(InboxCard, {
+ groups[group.id].length === 0 ? (0, import_react13.createElement)("p", { className: "bt-inbox-empty" }, t(group.empty)) : (0, import_react13.createElement)("div", { className: "bt-inbox-list" }, groups[group.id].map((card) => (0, import_react13.createElement)(InboxCard, {
key: card.id,
card,
roster,
@@ -7676,15 +7721,15 @@ function progressOf(task) {
}
// src/client/kanban.js
-var import_react13 = require("react");
+var import_react14 = require("react");
var POLL_MS3 = 15e3;
function BoardIcon() {
- return (0, import_react13.createElement)(
+ return (0, import_react14.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_react13.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
- (0, import_react13.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
- (0, import_react13.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 })
+ (0, import_react14.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
+ (0, import_react14.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
+ (0, import_react14.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 })
);
}
function shortId(id) {
@@ -7812,10 +7857,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_react13.useState)([]);
- const [groupTasks, setGroupTasks] = (0, import_react13.useState)([]);
+ const [own, setOwn] = (0, import_react14.useState)([]);
+ const [groupTasks, setGroupTasks] = (0, import_react14.useState)([]);
const key = scope?.kind === "group" ? `group:${scope.roomId}` : "own";
- (0, import_react13.useEffect)(() => {
+ (0, import_react14.useEffect)(() => {
let stopped = false;
const load = () => {
const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.());
@@ -7841,7 +7886,7 @@ function useBoardTasks(actions, scope) {
return { own, tasks, replace };
}
function RailButton({ row, selected, meta, onSelect }) {
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -7853,8 +7898,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_react13.createElement)("span", { className: "bt-board-rail-name" }, row.name),
- meta ? (0, import_react13.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null
+ (0, import_react14.createElement)("span", { className: "bt-board-rail-name" }, row.name),
+ meta ? (0, import_react14.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null
);
}
function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) {
@@ -7864,7 +7909,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"article",
{
className: "bt-board-card",
@@ -7880,35 +7925,35 @@ function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) {
},
onDragEnd: () => onDrag?.(null)
},
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"span",
{ className: "bt-board-card-top" },
- (0, import_react13.createElement)("span", { className: "bt-board-id" }, shortId(task.id)),
- (0, import_react13.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)),
- (0, import_react13.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status))
+ (0, import_react14.createElement)("span", { className: "bt-board-id" }, shortId(task.id)),
+ (0, import_react14.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)),
+ (0, import_react14.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status))
),
- (0, import_react13.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
- overall ? (0, import_react13.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null,
- partOf2 ? (0, import_react13.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null,
- task.note ? (0, import_react13.createElement)("span", { className: "bt-board-note" }, task.note) : null,
- question ? (0, import_react13.createElement)("span", { className: "bt-board-wait" }, question) : null,
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
+ overall ? (0, import_react14.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null,
+ partOf2 ? (0, import_react14.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null,
+ task.note ? (0, import_react14.createElement)("span", { className: "bt-board-note" }, task.note) : null,
+ question ? (0, import_react14.createElement)("span", { className: "bt-board-wait" }, question) : null,
+ (0, import_react14.createElement)(
"span",
{ className: "bt-board-foot" },
- who ? (0, import_react13.createElement)("span", { className: "bt-board-who" }, who) : null,
- (0, import_react13.createElement)(
+ who ? (0, import_react14.createElement)("span", { className: "bt-board-who" }, who) : null,
+ (0, import_react14.createElement)(
"span",
{ className: "bt-board-progress" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"span",
{ className: "bt-board-progress-bar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progress, "aria-label": t("Progress") },
- (0, import_react13.createElement)("span", { style: { width: `${progress}%` } })
+ (0, import_react14.createElement)("span", { style: { width: `${progress}%` } })
),
- (0, import_react13.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress }))
+ (0, import_react14.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress }))
)
),
- movable && column === "not-started" ? (0, import_react13.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_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null
+ movable && column === "not-started" ? (0, import_react14.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_react14.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) {
@@ -7918,7 +7963,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"section",
{
className: "bt-board-column",
@@ -7935,31 +7980,31 @@ function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag })
onDrag?.(null);
}
},
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"h3",
{ className: "bt-board-column-head" },
- (0, import_react13.createElement)("span", null, columnTitle(column)),
- (0, import_react13.createElement)("span", { className: "bt-board-count" }, String(tasks.length))
+ (0, import_react14.createElement)("span", null, columnTitle(column)),
+ (0, import_react14.createElement)("span", { className: "bt-board-count" }, String(tasks.length))
),
- dragging ? (0, import_react13.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null,
- tasks.length === 0 && !dragging ? (0, import_react13.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null,
- tree ? tree.nests.map((nest) => (0, import_react13.createElement)(
+ dragging ? (0, import_react14.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null,
+ tasks.length === 0 && !dragging ? (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null,
+ tree ? tree.nests.map((nest) => (0, import_react14.createElement)(
"div",
{ key: nest.task.id, className: "bt-board-nest" },
- (0, import_react13.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)),
- nest.children.map((child) => (0, import_react13.createElement)(
+ (0, import_react14.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)),
+ nest.children.map((child) => (0, import_react14.createElement)(
"div",
{ key: child.id, className: "bt-board-subtask", "data-parent": nest.task.id },
- (0, import_react13.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag))
+ (0, import_react14.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag))
))
)) : null,
- linked.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })),
- lanes ? lanesOf(plain, roster).map((lane) => (0, import_react13.createElement)(
+ linked.map((task) => (0, import_react14.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })),
+ lanes ? lanesOf(plain, roster).map((lane) => (0, import_react14.createElement)(
"div",
{ key: lane.id, className: "bt-board-lane" },
- (0, import_react13.createElement)("span", { className: "bt-board-lane-name" }, lane.name),
- lane.tasks.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
- )) : plain.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
+ (0, import_react14.createElement)("span", { className: "bt-board-lane-name" }, lane.name),
+ lane.tasks.map((task) => (0, import_react14.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
+ )) : plain.map((task) => (0, import_react14.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 }) {
@@ -7967,16 +8012,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"div",
{ className: "bt-board-body" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"aside",
{ className: "bt-board-rail", "aria-label": t("Task board") },
- (0, import_react13.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")),
- rail.bots.length === 0 ? (0, import_react13.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => {
+ (0, import_react14.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")),
+ rail.bots.length === 0 ? (0, import_react14.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_react13.createElement)(RailButton, {
+ return (0, import_react14.createElement)(RailButton, {
key: row.id,
row,
selected: row.id === selectedBot,
@@ -7984,26 +8029,26 @@ 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_react13.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null,
- rail.groups.map((row) => (0, import_react13.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))
+ rail.groups.length > 0 ? (0, import_react14.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null,
+ rail.groups.map((row) => (0, import_react14.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))
),
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-board-main" },
- error ? (0, import_react13.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null,
- scope?.kind === "bot" && onCreate ? (0, import_react13.createElement)(
+ error ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null,
+ scope?.kind === "bot" && onCreate ? (0, import_react14.createElement)(
"form",
{ className: "bt-board-add", onSubmit: (event) => {
event.preventDefault();
onCreate(draft);
} },
- (0, import_react13.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
- (0, import_react13.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
+ (0, import_react14.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
+ (0, import_react14.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
) : null,
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-board-columns" },
- ["not-started", "in-progress", "done"].map((column) => (0, import_react13.createElement)(BoardColumn, {
+ ["not-started", "in-progress", "done"].map((column) => (0, import_react14.createElement)(BoardColumn, {
key: column,
column,
tasks: columns[column],
@@ -8020,7 +8065,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -8031,8 +8076,8 @@ function KanbanRow({ wide, useUi, actions }) {
"aria-expanded": open,
onClick: () => actions.openBoard()
},
- (0, import_react13.createElement)("span", { className: "bt-board-mark" }, (0, import_react13.createElement)(BoardIcon)),
- wide ? (0, import_react13.createElement)("span", { className: "bt-board-label" }, label) : null
+ (0, import_react14.createElement)("span", { className: "bt-board-mark" }, (0, import_react14.createElement)(BoardIcon)),
+ wide ? (0, import_react14.createElement)("span", { className: "bt-board-label" }, label) : null
);
}
function scopeName(roster, scope) {
@@ -8042,12 +8087,12 @@ function scopeName(roster, scope) {
}
function KanbanDialog({ roster, actions, leaving = false }) {
const rail = boardRail(roster);
- const [scope, setScope] = (0, import_react13.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_react13.useState)(null);
- const [error, setError] = (0, import_react13.useState)("");
- const [draft, setDraft] = (0, import_react13.useState)("");
+ const [scope, setScope] = (0, import_react14.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_react14.useState)(null);
+ const [error, setError] = (0, import_react14.useState)("");
+ const [draft, setDraft] = (0, import_react14.useState)("");
const { own, tasks, replace } = useBoardTasks(actions, scope);
- const close = (0, import_react13.useCallback)(() => actions.closeBoard(), []);
+ const close = (0, import_react14.useCallback)(() => actions.closeBoard(), []);
useEscape(close);
const counts2 = {};
for (const task of own) {
@@ -8079,22 +8124,22 @@ function KanbanDialog({ roster, actions, leaving = false }) {
}
};
const name = scopeName(roster, scope);
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"div",
{ className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 },
- (0, import_react13.createElement)("div", { className: "bt-scrim", onClick: close }),
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)("div", { className: "bt-scrim", onClick: close }),
+ (0, import_react14.createElement)(
"div",
{ className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-mem-head" },
- (0, import_react13.createElement)("h2", { id: "bt-board-heading" }, t("Tasks")),
- (0, import_react13.createElement)("span", { className: "bt-mem-status" }, name),
- (0, import_react13.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react13.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react13.createElement)(CloseIcon))
+ (0, import_react14.createElement)("h2", { id: "bt-board-heading" }, t("Tasks")),
+ (0, import_react14.createElement)("span", { className: "bt-mem-status" }, name),
+ (0, import_react14.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react14.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react14.createElement)(CloseIcon))
),
- (0, import_react13.createElement)(BoardView, {
+ (0, import_react14.createElement)(BoardView, {
roster,
tasks,
rail,
@@ -8176,22 +8221,22 @@ var KANBAN_CSS = `
`;
// src/client/sidebar.js
-var import_react14 = require("react");
+var import_react15 = 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_react14.useEffect)(() => {
+ (0, import_react15.useEffect)(() => {
if (active && marked) actions.clearUnread(id);
}, [active, marked]);
}
-var PinMark = () => (0, import_react14.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react14.createElement)(PinMarkIcon));
+var PinMark = () => (0, import_react15.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react15.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_react14.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
+var workLine = (lead, tool, more) => (0, import_react15.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);
@@ -8222,7 +8267,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_react14.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react14.createElement)(
+ return (0, import_react15.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8234,15 +8279,15 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react14.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
- (0, import_react14.createElement)("span", { className: "bt-tile-name" }, shown),
+ (0, import_react15.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
+ (0, import_react15.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_react14.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
- bot.role ? (0, import_react14.createElement)("span", { className: "bt-chip" }, bot.role) : null
+ unread && !waiting && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
+ bot.role ? (0, import_react15.createElement)("span", { className: "bt-chip" }, bot.role) : null
));
}
- const unreadMark = wide && waiting && !active ? (0, import_react14.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
- const row = (0, import_react14.createElement)(
+ const unreadMark = wide && waiting && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react15.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
+ const row = (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8255,24 +8300,24 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react14.createElement)(BotAvatar, { bot, size: 36, state, main }),
- pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
- wide ? (0, import_react14.createElement)(
+ (0, import_react15.createElement)(BotAvatar, { bot, size: 36, state, main }),
+ pinned && !wide ? (0, import_react15.createElement)(PinMark) : null,
+ wide ? (0, import_react15.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"span",
{ className: "bt-row-name" },
- (0, import_react14.createElement)("span", { className: "bt-row-title" }, shown),
- bot.remote ? (0, import_react14.createElement)(RemoteMark, { remote: bot.remote }) : null,
- pinned ? (0, import_react14.createElement)(PinMark) : null
+ (0, import_react15.createElement)("span", { className: "bt-row-title" }, shown),
+ bot.remote ? (0, import_react15.createElement)(RemoteMark, { remote: bot.remote }) : null,
+ pinned ? (0, import_react15.createElement)(PinMark) : null
),
- secondLine ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
+ secondLine ? (0, import_react15.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_react14.createElement)("button", {
+ const settings = wide ? (0, import_react15.createElement)("button", {
type: "button",
className: "bt-row-settings",
"aria-label": settingsLabel,
@@ -8283,8 +8328,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
keepSidebarStill(event.currentTarget);
actions.openSettings("bots", id);
}
- }, (0, import_react14.createElement)(GearIcon)) : null;
- if (settings) return (0, import_react14.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ }, (0, import_react15.createElement)(GearIcon)) : null;
+ if (settings) return (0, import_react15.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
return row;
}
function keepSidebarStill(node) {
@@ -8297,14 +8342,14 @@ function keepSidebarStill(node) {
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
}
function RemoteMark({ remote }) {
- return (0, import_react14.createElement)(
+ return (0, import_react15.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_react14.createElement)(
+ (0, import_react15.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react14.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
- (0, import_react14.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ (0, import_react15.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
+ (0, import_react15.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
)
);
}
@@ -8316,15 +8361,15 @@ function isPastFaceRoom(room2) {
return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now();
}
function FaceMark() {
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"span",
{ className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react14.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
- (0, import_react14.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
- (0, import_react14.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
+ (0, import_react15.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
+ (0, import_react15.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
+ (0, import_react15.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
)
);
}
@@ -8349,7 +8394,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_react14.createElement)(
+ return (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8366,20 +8411,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react14.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
- pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
- wide ? (0, import_react14.createElement)(
+ (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
+ pinned && !wide ? (0, import_react15.createElement)(PinMark) : null,
+ wide ? (0, import_react15.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react14.createElement)("span", { className: "bt-row-name" }, (0, import_react14.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react14.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react14.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react14.createElement)(PinMark) : null),
- workingLine ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, preview) : null
+ (0, import_react15.createElement)("span", { className: "bt-row-name" }, (0, import_react15.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react15.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react15.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react15.createElement)(PinMark) : null),
+ workingLine ? (0, import_react15.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react15.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && alert && !active ? (0, import_react14.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ wide && alert && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react15.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
);
}
function AgentSessionRow({ session, agent, actions, renaming, index }) {
- const input = (0, import_react14.useRef)(null);
- (0, import_react14.useLayoutEffect)(() => {
+ const input = (0, import_react15.useRef)(null);
+ (0, import_react15.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
@@ -8400,10 +8445,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
}
})();
};
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react14.createElement)("input", {
+ (0, import_react15.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
@@ -8416,7 +8461,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
})
);
}
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8430,8 +8475,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_react14.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
- (0, import_react14.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
+ (0, import_react15.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
+ (0, import_react15.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
@@ -8466,10 +8511,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_react14.createElement)(
+ return (0, import_react15.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8484,30 +8529,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react14.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react14.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
- wide ? (0, import_react14.createElement)(
+ (0, import_react15.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react15.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
+ wide ? (0, import_react15.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react14.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
- preview ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, preview) : null
+ (0, import_react15.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
+ preview ? (0, import_react15.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ wide && unread && !active ? (0, import_react15.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react14.createElement)(
+ (0, import_react15.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_react14.createElement)(PlusIcon),
- (0, import_react14.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
+ (0, import_react15.createElement)(PlusIcon),
+ (0, import_react15.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
),
- sessions.map((session, index) => (0, import_react14.createElement)(AgentSessionRow, {
+ sessions.map((session, index) => (0, import_react15.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
@@ -8521,8 +8566,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
}
var GLIDE = springEasing(1e3, 63);
function useListGlide(listRef, layout, key) {
- const tops = (0, import_react14.useRef)({ layout, map: /* @__PURE__ */ new Map() });
- (0, import_react14.useLayoutEffect)(() => {
+ const tops = (0, import_react15.useRef)({ layout, map: /* @__PURE__ */ new Map() });
+ (0, import_react15.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
@@ -8539,7 +8584,7 @@ function useListGlide(listRef, layout, key) {
tops.current = { layout, map: next };
}, [layout, key]);
}
-var NodeActions = class extends import_react14.Component {
+var NodeActions = class extends import_react15.Component {
constructor(props) {
super(props);
this.state = { failed: false };
@@ -8551,32 +8596,32 @@ var NodeActions = class extends import_react14.Component {
console.error("flybot: nodeActions failed", error);
}
render() {
- return this.state.failed ? null : (0, import_react14.createElement)(this.props.component, { bot: this.props.bot });
+ return this.state.failed ? null : (0, import_react15.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_react14.createElement)(
- import_react14.Fragment,
+ return (0, import_react15.createElement)(
+ import_react15.Fragment,
null,
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"div",
{ className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
- children.length ? (0, import_react14.createElement)(
+ children.length ? (0, import_react15.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_react14.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
- ) : (0, import_react14.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
- (0, import_react14.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
- typeof actions === "function" ? (0, import_react14.createElement)(NodeActions, { component: actions, bot }) : null
+ (0, import_react15.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
+ ) : (0, import_react15.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
+ (0, import_react15.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
+ typeof actions === "function" ? (0, import_react15.createElement)(NodeActions, { component: actions, bot }) : null
),
- closed ? null : children.map((child) => (0, import_react14.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
+ closed ? null : children.map((child) => (0, import_react15.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_react14.useRef)(null);
+ const listRef = (0, import_react15.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
@@ -8585,7 +8630,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_react14.useMemo)(() => {
+ const order = (0, import_react15.useMemo)(() => {
const stamps = JSON.parse(updated);
const entries2 = [
// Own Bots, friends' Bots, groups and face rooms share one list. A remote chat
@@ -8600,7 +8645,7 @@ function TeamSidebar(props) {
}, [roster, updated]);
const main = roster.byId[roster.mainBotId];
const structure = useSidebarStructure();
- const [collapsed, setCollapsed] = (0, import_react14.useState)({});
+ const [collapsed, setCollapsed] = (0, import_react15.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;
@@ -8614,42 +8659,42 @@ function TeamSidebar(props) {
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react14.createElement)(
+ wide ? (0, import_react15.createElement)(
"div",
{ className: "bt-side-top" },
- (0, import_react14.createElement)("span", { className: "bt-brand" }, (0, import_react14.createElement)(BrandMark, { size: 22 }), (0, import_react14.createElement)(BrandName)),
- (0, import_react14.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react14.createElement)(SearchIcon)),
- (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react14.createElement)(PlusIcon))
- ) : (0, import_react14.createElement)(
- import_react14.Fragment,
+ (0, import_react15.createElement)("span", { className: "bt-brand" }, (0, import_react15.createElement)(BrandMark, { size: 22 }), (0, import_react15.createElement)(BrandName)),
+ (0, import_react15.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
+ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react15.createElement)(SearchIcon)),
+ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react15.createElement)(PlusIcon))
+ ) : (0, import_react15.createElement)(
+ import_react15.Fragment,
null,
- (0, import_react14.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react14.createElement)(PlusIcon))
+ (0, import_react15.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
+ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react15.createElement)(PlusIcon))
),
- wide && roster.readOnly ? (0, import_react14.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
- (0, import_react14.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
- (0, import_react14.createElement)(KanbanRow, { wide, useUi, actions }),
- (0, import_react14.createElement)(
+ wide && roster.readOnly ? (0, import_react15.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
+ (0, import_react15.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
+ (0, import_react15.createElement)(KanbanRow, { wide, useUi, actions }),
+ (0, import_react15.createElement)(
"div",
{ ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") },
- tree ? (0, import_react14.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react14.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react14.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react14.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_react14.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react14.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react14.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
- wide ? (0, import_react14.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react14.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
+ tree ? (0, import_react15.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react15.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react15.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react15.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_react15.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react15.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react15.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
+ wide ? (0, import_react15.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react15.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_react14.useState)(false);
- const [box, setBox] = (0, import_react14.useState)(null);
- const [error, setError] = (0, import_react14.useState)("");
- const [dshError, setDshError] = (0, import_react14.useState)("");
- const button = (0, import_react14.useRef)(null);
- (0, import_react14.useEffect)(() => {
+ const [open, setOpen] = (0, import_react15.useState)(false);
+ const [box, setBox] = (0, import_react15.useState)(null);
+ const [error, setError] = (0, import_react15.useState)("");
+ const [dshError, setDshError] = (0, import_react15.useState)("");
+ const button = (0, import_react15.useRef)(null);
+ (0, import_react15.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;
@@ -8692,10 +8737,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_react14.createElement)(
- import_react14.Fragment,
+ return (0, import_react15.createElement)(
+ import_react15.Fragment,
null,
- (0, import_react14.createElement)("button", {
+ (0, import_react15.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
@@ -8705,21 +8750,21 @@ function UpdateButton({ useUpdate, actions }) {
"aria-haspopup": "dialog",
"aria-expanded": open,
onClick: toggle
- }, (0, import_react14.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react14.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react14.createElement)(
+ }, (0, import_react15.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react15.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
+ open && box ? (0, import_react_dom3.createPortal)((0, import_react15.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_react14.createElement)(
- import_react14.Fragment,
+ update?.available || phase !== "idle" ? (0, import_react15.createElement)(
+ import_react15.Fragment,
null,
- (0, import_react14.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
- (0, import_react14.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_react14.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
+ (0, import_react15.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_react15.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
+ (0, import_react15.createElement)(
"div",
{ className: "bt-update-actions" },
- update.notesUrl ? (0, import_react14.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
- phase === "installed" ? null : (0, import_react14.createElement)("button", {
+ update.notesUrl ? (0, import_react15.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
+ phase === "installed" ? null : (0, import_react15.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
@@ -8727,17 +8772,17 @@ function UpdateButton({ useUpdate, actions }) {
}, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
)
) : null,
- dshRow ? (0, import_react14.createElement)(
+ dshRow ? (0, import_react15.createElement)(
"div",
{ className: "bt-update-dsh" },
- (0, import_react14.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
- (0, import_react14.createElement)("div", { className: "bt-update-text" }, dshText),
- dshMessage ? (0, import_react14.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
+ (0, import_react15.createElement)("div", { className: "bt-update-text" }, dshText),
+ dshMessage ? (0, import_react15.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
+ (0, import_react15.createElement)(
"div",
{ className: "bt-update-actions" },
- dsh.kind === "app" ? dsh.available ? (0, import_react14.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_react14.createElement)("button", {
+ dsh.kind === "app" ? dsh.available ? (0, import_react15.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_react15.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
@@ -8750,23 +8795,23 @@ function UpdateButton({ useUpdate, actions }) {
}
function ConnectApps({ wide, actions }) {
if (!wide) return null;
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"button",
{ type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() },
- (0, import_react14.createElement)(PlugIcon),
+ (0, import_react15.createElement)(PlugIcon),
t("Connect apps")
)
);
}
function YouAvatar() {
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"span",
{ className: "bt-you-wrap" },
- (0, import_react14.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react14.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
- (0, import_react14.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
+ (0, import_react15.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react15.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
+ (0, import_react15.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
);
}
var openShellSettings = null;
@@ -8798,7 +8843,7 @@ function openHarnessOnboarding(id) {
var MODELS_STEP_ID = "flybot-models";
var MODELS_STEP_ORDER = 1e3;
function ModelsSettingsStep({ explicit, complete, openSection }) {
- (0, import_react14.useLayoutEffect)(() => {
+ (0, import_react15.useLayoutEffect)(() => {
if (explicit) openSection("models");
complete();
}, []);
@@ -8836,32 +8881,32 @@ function pressSettingsShortcut() {
}));
}
function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) {
- const [open, setOpen] = (0, import_react14.useState)(false);
- (0, import_react14.useLayoutEffect)(() => {
+ const [open, setOpen] = (0, import_react15.useState)(false);
+ (0, import_react15.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
}, [seat]);
- const [box, setBox] = (0, import_react14.useState)(null);
- const button = (0, import_react14.useRef)(null);
- (0, import_react14.useEffect)(() => {
+ const [box, setBox] = (0, import_react15.useState)(null);
+ const button = (0, import_react15.useRef)(null);
+ (0, import_react15.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react14.useEffect)(() => {
+ (0, import_react15.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
if (openShellSettings === openSettings) openShellSettings = null;
};
}, [openSettings]);
- (0, import_react14.useEffect)(() => {
+ (0, import_react15.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
if (openShellOnboarding === openOnboarding) openShellOnboarding = null;
};
}, [openOnboarding]);
- (0, import_react14.useEffect)(() => {
+ (0, import_react15.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;
@@ -8898,23 +8943,23 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
[t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")]
]
];
- return (0, import_react14.createElement)(
- import_react14.Fragment,
+ return (0, import_react15.createElement)(
+ import_react15.Fragment,
null,
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"button",
{ ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle },
- (0, import_react14.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react14.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
+ (0, import_react15.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react15.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react14.createElement)(
+ open && box ? (0, import_react_dom3.createPortal)((0, import_react15.createElement)(
"div",
{ className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box },
- (0, import_react14.createElement)(FlyBotAccountHeader, { actions }),
- groups.map((group, index) => (0, import_react14.createElement)(
- import_react14.Fragment,
+ (0, import_react15.createElement)(FlyBotAccountHeader, { actions }),
+ groups.map((group, index) => (0, import_react15.createElement)(
+ import_react15.Fragment,
{ key: index },
- index > 0 ? (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, hint]) => (0, import_react14.createElement)("button", {
+ index > 0 ? (0, import_react15.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, hint]) => (0, import_react15.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -8922,7 +8967,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
setOpen(false);
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react14.createElement)(Icon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react14.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
+ }, (0, import_react15.createElement)(Icon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react15.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
))
), document.body) : null
);
@@ -8932,8 +8977,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_react14.useRef)(null);
- (0, import_react14.useEffect)(() => {
+ const lastBot = (0, import_react15.useRef)(null);
+ (0, import_react15.useEffect)(() => {
if (!roster.ready || !roster.mainBotId) return void 0;
const [id, blank3] = mainView.split("|");
const bot = id === "" ? void 0 : roster.byId[id];
@@ -8954,16 +8999,16 @@ function useLandOnMainBot(roster, useSessions, actions) {
function FooterAccount(props) {
const seated = props.useUi((value) => value.accountSeated === true);
if (seated) return null;
- return (0, import_react14.createElement)("span", { className: "bt-foot-account" }, (0, import_react14.createElement)(AccountLauncher, { ...props, seat: false }));
+ return (0, import_react15.createElement)("span", { className: "bt-foot-account" }, (0, import_react15.createElement)(AccountLauncher, { ...props, seat: false }));
}
function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface((value) => value.mode);
if (mode !== "agent") return null;
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"button",
{ type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") },
- (0, import_react14.createElement)(FlyBotMark, { size: 20 }),
- wide ? (0, import_react14.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
+ (0, import_react15.createElement)(FlyBotMark, { size: 20 }),
+ wide ? (0, import_react15.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -8977,10 +9022,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_react14.createElement)("span", { className: "bt-brand-mark" }, (0, import_react14.createElement)(FlyBotMark, { size }));
+ return (0, import_react15.createElement)("span", { className: "bt-brand-mark" }, (0, import_react15.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react14.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
+ return (0, import_react15.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
}
var SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
@@ -9054,15 +9099,15 @@ var SIDEBAR_CSS = `
`;
// src/client/header.js
-var import_react15 = require("react");
+var import_react16 = require("react");
var import_react_dom4 = require("react-dom");
-var GoArrow = () => (0, import_react15.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_react15.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" }));
+var GoArrow = () => (0, import_react16.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_react16.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_react15.useState)(false);
- (0, import_react15.useEffect)(() => {
+ const [shown, setShown] = (0, import_react16.useState)(false);
+ (0, import_react16.useEffect)(() => {
const left = seen + MEMORY_NOTICE_MS - Date.now();
setShown(seen > 0 && left > 0);
if (seen === 0 || left <= 0) return void 0;
@@ -9072,16 +9117,16 @@ function useMemoryNotice(ids, useMemoryNews) {
return shown;
}
function MemoryNotice({ shown }) {
- const inner = (0, import_react15.useRef)(null);
- const [width, setWidth] = (0, import_react15.useState)(0);
+ const inner = (0, import_react16.useRef)(null);
+ const [width, setWidth] = (0, import_react16.useState)(0);
const label = t("Memory updated");
- (0, import_react15.useLayoutEffect)(() => {
+ (0, import_react16.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react15.createElement)(
+ return (0, import_react16.createElement)(
"span",
{ className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } },
- (0, import_react15.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react15.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react15.createElement)(MemoryIcon)), label)
+ (0, import_react16.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react16.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react16.createElement)(MemoryIcon)), label)
);
}
function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
@@ -9097,7 +9142,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_react15.useEffect)(() => {
+ (0, import_react16.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");
@@ -9116,10 +9161,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_react15.createElement)(
+ return (0, import_react16.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react15.createElement)(
+ (0, import_react16.createElement)(
"button",
{
type: "button",
@@ -9129,19 +9174,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_react15.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
- (0, import_react15.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_react15.createElement)(MemoryNotice, { shown: memoryNotice }),
- (0, import_react15.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react15.createElement)(GoArrow))
+ bot ? (0, import_react16.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
+ (0, import_react16.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_react16.createElement)(MemoryNotice, { shown: memoryNotice }),
+ (0, import_react16.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react16.createElement)(GoArrow))
),
- (0, import_react15.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
- (0, import_react15.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
- (0, import_react15.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })
+ (0, import_react16.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
+ (0, import_react16.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
+ (0, import_react16.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })
);
}
function useTranscriptView() {
- const [view, setView] = (0, import_react15.useState)(null);
- (0, import_react15.useEffect)(() => {
+ const [view, setView] = (0, import_react16.useState)(null);
+ (0, import_react16.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
@@ -9206,22 +9251,22 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
});
const view = useTranscriptView();
const unpinned = view?.unpinned === true;
- const [release, setRelease] = (0, import_react15.useState)({ base: null, dismissed: false });
- const [above, setAbove] = (0, import_react15.useState)({ seen: false, dismissed: false });
- (0, import_react15.useEffect)(() => {
+ const [release, setRelease] = (0, import_react16.useState)({ base: null, dismissed: false });
+ const [above, setAbove] = (0, import_react16.useState)({ seen: false, dismissed: false });
+ (0, import_react16.useEffect)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react15.useEffect)(() => {
+ (0, import_react16.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_react15.useEffect)(() => {
+ (0, import_react16.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_react15.useEffect)(() => {
+ (0, import_react16.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
@@ -9229,16 +9274,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_react15.createElement)(
+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react16.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react15.createElement)(
+ (0, import_react16.createElement)(
"button",
{ type: "button", className: "bt-news-jump", onClick: onJump },
- (0, import_react15.createElement)("span", { className: "bt-news-ico" }, (0, import_react15.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
+ (0, import_react16.createElement)("span", { className: "bt-news-ico" }, (0, import_react16.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react15.createElement)("button", {
+ (0, import_react16.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
@@ -9246,7 +9291,7 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
event.stopPropagation();
onDismiss();
}
- }, (0, import_react15.createElement)(CloseIcon))
+ }, (0, import_react16.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
@@ -9256,8 +9301,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_react15.createElement)(
- import_react15.Fragment,
+ (0, import_react16.createElement)(
+ import_react16.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
@@ -9365,18 +9410,18 @@ function installHeaderEffects({ sessions, roster, actions, doc = globalThis.docu
}
// src/client/composer.js
-var import_react16 = require("react");
+var import_react17 = 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_react16.useState)(false);
- const [note, setNote] = (0, import_react16.useState)("");
- const recognition = (0, import_react16.useRef)(null);
- const latest = (0, import_react16.useRef)(onText);
+ const [listening, setListening] = (0, import_react17.useState)(false);
+ const [note, setNote] = (0, import_react17.useState)("");
+ const recognition = (0, import_react17.useRef)(null);
+ const latest = (0, import_react17.useRef)(onText);
latest.current = onText;
- (0, import_react16.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => () => recognition.current?.abort(), []);
+ (0, import_react17.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
@@ -9430,14 +9475,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_react16.createElement)(
+ return (0, import_react17.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_react16.createElement)(
+ return (0, import_react17.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 })
@@ -9448,7 +9493,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_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -9466,10 +9511,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
}, [quote?.token]);
if (sessionId === void 0) return null;
const pluginMic = actions.speech() !== void 0;
- return (0, import_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- pluginMic ? null : (0, import_react16.createElement)("button", {
+ pluginMic ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -9477,16 +9522,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_react16.createElement)(MicIcon)),
- (0, import_react16.createElement)("button", {
+ }, (0, import_react17.createElement)(MicIcon)),
+ (0, import_react17.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_react16.createElement)(WaveIcon, { size: 18 })),
- dictation.note ? (0, import_react16.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
+ }, (0, import_react17.createElement)(WaveIcon, { size: 18 })),
+ dictation.note ? (0, import_react17.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
);
}
function menuIcons(roster) {
@@ -9494,17 +9539,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_react16.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react17.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_react16.useState)(null);
+ const [open, setOpen] = (0, import_react17.useState)(null);
const [shown, leaving] = useLinger(open, 120);
- const button = (0, import_react16.useRef)(null);
- const menu = (0, import_react16.useRef)(null);
- (0, import_react16.useEffect)(() => {
+ const button = (0, import_react17.useRef)(null);
+ const menu = (0, import_react17.useRef)(null);
+ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null);
@@ -9545,10 +9590,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
};
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- 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-plus",
@@ -9558,8 +9603,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
"aria-expanded": Boolean(open),
onMouseDown: keepComposerFocus,
onClick: toggle
- }, (0, import_react16.createElement)(PlusIcon)),
- shown ? (0, import_react_dom5.createPortal)((0, import_react16.createElement)(
+ }, (0, import_react17.createElement)(PlusIcon)),
+ shown ? (0, import_react_dom5.createPortal)((0, import_react17.createElement)(
"div",
{
ref: menu,
@@ -9569,9 +9614,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
style: { left: shown.left, bottom: shown.bottom },
"data-leaving": leaving || void 0
},
- (0, import_react16.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react16.createElement)(PaperclipIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
- canReference ? (0, import_react16.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react16.createElement)(ChatsIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
- (0, import_react16.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react16.createElement)(ClockIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
+ (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react17.createElement)(PaperclipIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
+ canReference ? (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react17.createElement)(ChatsIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
+ (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react17.createElement)(ClockIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
), document.body) : null
);
}
@@ -9925,7 +9970,7 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) {
}
// src/client/model-menu.js
-var import_react17 = require("react");
+var import_react18 = require("react");
var import_react_dom6 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
@@ -9948,8 +9993,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_react17.useState)(void 0);
- (0, import_react17.useEffect)(() => {
+ const [fetched, setFetched] = (0, import_react18.useState)(void 0);
+ (0, import_react18.useEffect)(() => {
setFetched(void 0);
if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
let live = true;
@@ -9974,12 +10019,12 @@ function effortRows(reasoning) {
];
}
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react17.createElement)(
- import_react17.Fragment,
+ return (0, import_react18.createElement)(
+ import_react18.Fragment,
null,
- (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
- effortRows(reasoning).map((row) => (0, import_react17.createElement)(
+ (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
+ effortRows(reasoning).map((row) => (0, import_react18.createElement)(
"button",
{
key: row.effort ?? "default",
@@ -9988,17 +10033,17 @@ function EffortRows({ reasoning, current, onPick }) {
"aria-checked": row.effort === current,
onClick: () => onPick(row.effort)
},
- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, row.label),
- (0, import_react17.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react17.createElement)(CheckIcon) : null)
+ (0, import_react18.createElement)("span", { className: "bt-menu-label" }, row.label),
+ (0, import_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react18.createElement)(CheckIcon) : null)
))
);
}
function ModelRows({ roster, current, onPick }) {
- return groupModels(roster.models).map(([provider, list]) => (0, import_react17.createElement)(
- import_react17.Fragment,
+ return groupModels(roster.models).map(([provider, list]) => (0, import_react18.createElement)(
+ import_react18.Fragment,
{ key: list[0].provider ?? provider },
- (0, import_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
- list.map((model) => (0, import_react17.createElement)(
+ (0, import_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
+ list.map((model) => (0, import_react18.createElement)(
"button",
{
key: model.ref,
@@ -10008,20 +10053,20 @@ function ModelRows({ roster, current, onPick }) {
title: model.ref,
onClick: () => onPick(model.ref)
},
- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, model.name),
- model.ref === roster.defaultModel ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
- (0, import_react17.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react17.createElement)(CheckIcon) : null)
+ (0, import_react18.createElement)("span", { className: "bt-menu-label" }, model.name),
+ model.ref === roster.defaultModel ? (0, import_react18.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
+ (0, import_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react18.createElement)(CheckIcon) : null)
))
));
}
-var Caret = () => (0, import_react17.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_react17.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
+var Caret = () => (0, import_react18.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_react18.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
function useAnchoredMenu() {
- const [place, setPlace] = (0, import_react17.useState)(null);
+ const [place, setPlace] = (0, import_react18.useState)(null);
const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS);
- const button = (0, import_react17.useRef)(null);
- const menu = (0, import_react17.useRef)(null);
+ const button = (0, import_react18.useRef)(null);
+ const menu = (0, import_react18.useRef)(null);
const close = () => setPlace(null);
- (0, import_react17.useEffect)(() => {
+ (0, import_react18.useEffect)(() => {
if (place === null) return void 0;
const onDown = (event) => {
if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return;
@@ -10084,10 +10129,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_react17.createElement)(
- import_react17.Fragment,
+ return (0, import_react18.createElement)(
+ import_react18.Fragment,
null,
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"button",
{
ref: button,
@@ -10100,38 +10145,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_react17.createElement)(
+ (0, import_react18.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react17.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
- shown ? (0, import_react17.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_react18.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
+ shown ? (0, import_react18.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_react17.createElement)(Caret)
+ (0, import_react18.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react17.createElement)(
+ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react18.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_react17.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react17.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
- effortRows(reasoning).length > 0 ? (0, import_react17.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
- (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
+ models.length > 0 ? (0, import_react18.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react18.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
+ effortRows(reasoning).length > 0 ? (0, import_react18.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
+ (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
close();
openHarnessModels();
- } }, (0, import_react17.createElement)(SlidersIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
+ } }, (0, import_react18.createElement)(SlidersIcon), (0, import_react18.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_react17.useState)("");
- const [error, setError] = (0, import_react17.useState)("");
+ const [busy, setBusy] = (0, import_react18.useState)("");
+ const [error, setError] = (0, import_react18.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react17.useEffect)(() => {
+ (0, import_react18.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_react17.createElement)(RemoteGoneDock, { bot }) : null;
+ if (bot.remote) return bot.remote.gone ? (0, import_react18.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;
@@ -10144,12 +10189,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_react17.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react17.createElement)(KeyHelpLink, { key: "key-help" })] : [
- ...groupModels(models).map(([provider, list]) => (0, import_react17.createElement)(
+ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react18.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react18.createElement)(KeyHelpLink, { key: "key-help" })] : [
+ ...groupModels(models).map(([provider, list]) => (0, import_react18.createElement)(
"div",
{ key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
- (0, import_react17.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
- (0, import_react17.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react17.createElement)("button", {
+ (0, import_react18.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
+ (0, import_react18.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react18.createElement)("button", {
key: model.ref,
type: "button",
className: "bt-model-chip",
@@ -10158,46 +10203,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_react17.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
+ }, model.name, busy === model.ref ? (0, import_react18.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
)),
- (0, import_react17.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react17.createElement)(SlidersIcon), t("Manage models"))
+ (0, import_react18.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react18.createElement)(SlidersIcon), t("Manage models"))
];
- return (0, import_react17.createElement)(
+ return (0, import_react18.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react17.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react17.createElement)("div", { className: "bt-bubble" }, text)
+ (0, import_react18.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react18.createElement)("div", { className: "bt-bubble" }, text)
)
)
),
- (0, import_react17.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
- error ? (0, import_react17.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
+ (0, import_react18.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
+ error ? (0, import_react18.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
);
}
function RemoteGoneDock({ bot }) {
- return (0, import_react17.createElement)(
+ return (0, import_react18.createElement)(
"div",
{ className: "bt-model-dock", "data-flybot": "remote-gone" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react17.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react17.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_react18.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react18.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 }))
)
)
)
@@ -10212,7 +10257,7 @@ function setupChoices(actions) {
}
var KEY_HELP_URL = "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/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md";
function KeyHelpLink() {
- return (0, import_react17.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_react18.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 ?? "");
@@ -10236,22 +10281,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_react17.createElement)(
+ return (0, import_react18.createElement)(
"div",
{ className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
- (0, import_react17.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
+ (0, import_react18.createElement)(
"div",
{ className: "bt-turn-error-copy" },
- (0, import_react17.createElement)("span", { className: "bt-turn-error-title" }, title),
- (0, import_react17.createElement)("span", { className: "bt-turn-error-message" }, message),
- code ? (0, import_react17.createElement)("code", { className: "bt-turn-error-code" }, code) : null
+ (0, import_react18.createElement)("span", { className: "bt-turn-error-title" }, title),
+ (0, import_react18.createElement)("span", { className: "bt-turn-error-message" }, message),
+ code ? (0, import_react18.createElement)("code", { className: "bt-turn-error-code" }, code) : null
),
- choices.length > 0 ? (0, import_react17.createElement)(
+ choices.length > 0 ? (0, import_react18.createElement)(
"div",
{ className: "bt-turn-error-actions" },
- choices.map(([label, run]) => (0, import_react17.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
- deepseek ? (0, import_react17.createElement)(KeyHelpLink, { key: "key-help" }) : null
+ choices.map(([label, run]) => (0, import_react18.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
+ deepseek ? (0, import_react18.createElement)(KeyHelpLink, { key: "key-help" }) : null
) : null
);
}
@@ -10259,14 +10304,14 @@ function DeepSeekSignIn({ useUi, actions }) {
const open = useUi((state) => state.signIn);
const entry = open ? actions.signInEntry?.() : null;
if (!entry) return null;
- return (0, import_react17.createElement)(SignInHost, { key: open, entry, actions });
+ return (0, import_react18.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react17.useState)(() => entry.inject?.() ?? {});
+ const [ops] = (0, import_react18.useState)(() => entry.inject?.() ?? {});
const close = () => actions.closeSignIn();
- const useHook = (source) => (selector) => (0, import_react17.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
- const [hooks] = (0, import_react17.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
- return (0, import_react17.createElement)(entry.component, {
+ const useHook = (source) => (selector) => (0, import_react18.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
+ const [hooks] = (0, import_react18.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
+ return (0, import_react18.createElement)(entry.component, {
...ops,
...hooks,
t: actions.accountT(),
@@ -10325,7 +10370,7 @@ var MODEL_CSS = `
`;
// src/client/model-test.js
-var import_react18 = require("react");
+var import_react19 = 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.",
@@ -10350,12 +10395,12 @@ function testOutcome(result, modelName) {
}
var testModel = (id) => call("test-model", { id });
function ModelTestRow({ bot, modelName, test = testModel }) {
- const [state, setState] = (0, import_react18.useState)({ busy: false, outcome: void 0 });
- const live = (0, import_react18.useRef)(true);
- (0, import_react18.useEffect)(() => () => {
+ const [state, setState] = (0, import_react19.useState)({ busy: false, outcome: void 0 });
+ const live = (0, import_react19.useRef)(true);
+ (0, import_react19.useEffect)(() => () => {
live.current = false;
}, []);
- (0, import_react18.useEffect)(() => {
+ (0, import_react19.useEffect)(() => {
setState({ busy: false, outcome: void 0 });
}, [bot.id, bot.model]);
const start = () => {
@@ -10369,21 +10414,21 @@ function ModelTestRow({ bot, modelName, test = testModel }) {
});
};
const { busy, outcome } = state;
- return (0, import_react18.createElement)(
+ return (0, import_react19.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-model-test" },
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
+ (0, import_react19.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_react18.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ outcome?.code ? (0, import_react19.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
)
),
- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
+ (0, import_react19.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 = `
@@ -10395,17 +10440,17 @@ var MODEL_TEST_CSS = `
`;
// src/client/folder-menu.js
-var import_react19 = require("react");
+var import_react20 = require("react");
var import_react_dom7 = require("react-dom");
var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path);
-var Tick = ({ on }) => (0, import_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react19.createElement)(CheckIcon) : null);
+var Tick = ({ on }) => (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react20.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_react19.useState)(null);
- const [view, setView] = (0, import_react19.useState)("list");
- const [draft, setDraft] = (0, import_react19.useState)("");
- const [listing, setListing] = (0, import_react19.useState)(null);
- const [problem, setProblem] = (0, import_react19.useState)("");
+ const [info, setInfo] = (0, import_react20.useState)(null);
+ const [view, setView] = (0, import_react20.useState)("list");
+ const [draft, setDraft] = (0, import_react20.useState)("");
+ const [listing, setListing] = (0, import_react20.useState)(null);
+ const [problem, setProblem] = (0, import_react20.useState)("");
const fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
@@ -10438,35 +10483,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_react19.createElement)(
+ const listView = (0, import_react20.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react19.createElement)(
+ (0, import_react20.createElement)(
"button",
{ type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") },
- (0, import_react19.createElement)("span", { className: "bt-menu-label" }, fallback),
- (0, import_react19.createElement)(Tick, { on: !value })
+ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, fallback),
+ (0, import_react20.createElement)(Tick, { on: !value })
),
- folders.length > 0 ? (0, import_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
- folders.map((folder) => (0, import_react19.createElement)(
+ folders.length > 0 ? (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
+ folders.map((folder) => (0, import_react20.createElement)(
"button",
{ key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) },
- (0, import_react19.createElement)("span", { className: "bt-folder-copy" }, (0, import_react19.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react19.createElement)("span", { className: "bt-folder-path" }, folder.path)),
- (0, import_react19.createElement)(Tick, { on: folder.path === value })
+ (0, import_react20.createElement)("span", { className: "bt-folder-copy" }, (0, import_react20.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react20.createElement)("span", { className: "bt-folder-path" }, folder.path)),
+ (0, import_react20.createElement)(Tick, { on: folder.path === value })
)),
- (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- info?.picker === "native" ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
- info?.picker === "browse" ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
- (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
+ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ info?.picker === "native" ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
+ info?.picker === "browse" ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
setDraft(value ?? "");
setView("type");
- } }, (0, import_react19.createElement)(PencilIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
+ } }, (0, import_react20.createElement)(PencilIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
);
- const typeView = (0, import_react19.createElement)(
+ const typeView = (0, import_react20.createElement)(
"div",
{ key: "type", className: "bt-folder-view" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react19.createElement)(ChevronLeftIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react19.createElement)("input", {
+ (0, import_react20.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react20.createElement)(ChevronLeftIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
+ (0, import_react20.createElement)("input", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
@@ -10478,33 +10523,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
if (event.key === "Enter" && !event.nativeEvent.isComposing) typed();
}
}),
- (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
+ (0, import_react20.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_react19.createElement)(
+ const browseView = listing ? (0, import_react20.createElement)(
"div",
{ key: `browse:${listing.path}`, className: "bt-folder-view" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react19.createElement)(ChevronLeftIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react19.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
- parent ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
- inside.map((entry) => (0, import_react19.createElement)(
+ (0, import_react20.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react20.createElement)(ChevronLeftIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
+ (0, import_react20.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
+ parent ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
+ inside.map((entry) => (0, import_react20.createElement)(
"button",
{ key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) },
- (0, import_react19.createElement)(FolderIcon),
- (0, import_react19.createElement)("span", { className: "bt-menu-label" }, entry.name),
- (0, import_react19.createElement)(ChevronRightIcon)
+ (0, import_react20.createElement)(FolderIcon),
+ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, entry.name),
+ (0, import_react20.createElement)(ChevronRightIcon)
)),
- inside.length === 0 ? (0, import_react19.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
- listing.truncated ? (0, import_react19.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
- (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder"))
+ inside.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
+ listing.truncated ? (0, import_react20.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
+ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react20.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_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,
@@ -10517,19 +10562,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
title: value,
onClick: toggle
},
- (0, import_react19.createElement)(
+ (0, import_react20.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react19.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
- value ? (0, import_react19.createElement)("span", { className: "bt-model-provider" }, value) : null
+ (0, import_react20.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
+ value ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, value) : null
),
- (0, import_react19.createElement)(Caret)
+ (0, import_react20.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react19.createElement)(
+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react20.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_react19.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
+ problem ? (0, import_react20.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
@@ -10549,7 +10594,7 @@ var FOLDER_CSS = `
`;
// src/client/schedules.js
-var import_react20 = require("react");
+var import_react21 = require("react");
var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
@@ -10558,8 +10603,8 @@ function publish(view) {
for (const listener of listeners) listener(view);
}
function useSchedules(actions) {
- const [view, setView] = (0, import_react20.useState)(shared);
- (0, import_react20.useEffect)(() => {
+ const [view, setView] = (0, import_react21.useState)(shared);
+ (0, import_react21.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
@@ -10631,9 +10676,9 @@ function lastRunText(run) {
}
}
function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
- const [confirming, setConfirming] = (0, import_react20.useState)(false);
- const [busy, setBusy] = (0, import_react20.useState)(false);
- (0, import_react20.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react21.useState)(false);
+ const [busy, setBusy] = (0, import_react21.useState)(false);
+ (0, import_react21.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -10654,7 +10699,7 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
onView(next);
}, () => setBusy(false));
};
- const [retryError, setRetryError] = (0, import_react20.useState)("");
+ const [retryError, setRetryError] = (0, import_react21.useState)("");
const retry = () => {
setBusy(true);
setRetryError("");
@@ -10670,24 +10715,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_react20.createElement)(
+ return (0, import_react21.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_react20.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
- (0, import_react20.createElement)(
+ showBot ? (0, import_react21.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
+ (0, import_react21.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react20.createElement)("span", { className: "bt-task-title" }, task.title),
- (0, import_react20.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_react20.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
- last ? (0, import_react20.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
- retryError ? (0, import_react20.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
+ (0, import_react21.createElement)("span", { className: "bt-task-title" }, task.title),
+ (0, import_react21.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_react21.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
+ last ? (0, import_react21.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
+ retryError ? (0, import_react21.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
),
- (0, import_react20.createElement)("span", { className: "bt-task-next" }, nextText(task)),
- failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react20.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_react20.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react20.createElement)(PencilIcon)) : null,
- stoppable ? (0, import_react20.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
- (0, import_react20.createElement)("button", {
+ (0, import_react21.createElement)("span", { className: "bt-task-next" }, nextText(task)),
+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react21.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_react21.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react21.createElement)(PencilIcon)) : null,
+ stoppable ? (0, import_react21.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
+ (0, import_react21.createElement)("button", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
@@ -10695,7 +10740,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_react20.createElement)(TrashIcon))
+ }, confirming ? t("Delete") : (0, import_react21.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
@@ -10728,17 +10773,17 @@ function formStart(task) {
}
function TaskForm({ roster, actions, start, task, onDone, onCancel }) {
const first = formStart(task);
- const [target, setTarget] = (0, import_react20.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
- const [title, setTitle] = (0, import_react20.useState)(first.title);
- const [prompt, setPrompt] = (0, import_react20.useState)(first.prompt);
- const [kind, setKind] = (0, import_react20.useState)(first.kind);
- const [at, setAt] = (0, import_react20.useState)(first.at);
- const [time, setTime] = (0, import_react20.useState)(first.time);
- const [weekdays, setWeekdays] = (0, import_react20.useState)(first.weekdays);
- const [count, setCount] = (0, import_react20.useState)(first.count);
- const [unit, setUnit] = (0, import_react20.useState)(first.unit);
- const [busy, setBusy] = (0, import_react20.useState)(false);
- const [error, setError] = (0, import_react20.useState)("");
+ const [target, setTarget] = (0, import_react21.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
+ const [title, setTitle] = (0, import_react21.useState)(first.title);
+ const [prompt, setPrompt] = (0, import_react21.useState)(first.prompt);
+ const [kind, setKind] = (0, import_react21.useState)(first.kind);
+ const [at, setAt] = (0, import_react21.useState)(first.at);
+ const [time, setTime] = (0, import_react21.useState)(first.time);
+ const [weekdays, setWeekdays] = (0, import_react21.useState)(first.weekdays);
+ const [count, setCount] = (0, import_react21.useState)(first.count);
+ const [unit, setUnit] = (0, import_react21.useState)(first.unit);
+ const [busy, setBusy] = (0, import_react21.useState)(false);
+ const [error, setError] = (0, import_react21.useState)("");
const timing = () => {
if (kind === "cron") return { kind: "keep" };
if (kind === "at") return { kind, at: new Date(at).toISOString() };
@@ -10765,69 +10810,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_react20.createElement)(
+ return (0, import_react21.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
+ (0, import_react21.createElement)(
"select",
{ className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) },
- (0, import_react20.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react20.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
- roster.rooms.length > 0 ? (0, import_react20.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react20.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
+ (0, import_react21.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react21.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
+ roster.rooms.length > 0 ? (0, import_react21.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react21.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
)
),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react20.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
+ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
+ (0, import_react21.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
- (0, import_react20.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
+ (0, import_react21.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_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("When"))),
- (0, import_react20.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react20.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("When"))),
+ (0, import_react21.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react21.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
+ (0, import_react21.createElement)(
"div",
{ key: kind, className: "bt-task-when bt-veil" },
- kind === "cron" ? (0, import_react20.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
- kind === "at" ? (0, import_react20.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_react20.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react20.createElement)("button", {
+ kind === "cron" ? (0, import_react21.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
+ kind === "at" ? (0, import_react21.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_react21.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react21.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_react20.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_react20.createElement)(
- import_react20.Fragment,
+ kind === "daily" || kind === "weekly" ? (0, import_react21.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_react21.createElement)(
+ import_react21.Fragment,
null,
- (0, import_react20.createElement)("span", { className: "bt-task-every" }, t("Every")),
- (0, import_react20.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_react20.createElement)(
+ (0, import_react21.createElement)("span", { className: "bt-task-every" }, t("Every")),
+ (0, import_react21.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_react21.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_react20.createElement)("option", { key: id, value: id }, t(label)))
+ UNITS.map(([id, , label]) => (0, import_react21.createElement)("option", { key: id, value: id }, t(label)))
)
) : null
)
),
- error ? (0, import_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
- (0, import_react20.createElement)(
+ error ? (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
+ (0, import_react21.createElement)(
"div",
{ className: "bt-set-row bt-task-foot" },
- (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ (0, import_react21.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")
@@ -10840,21 +10885,21 @@ function openPlugins(actions) {
actions.selectPanel("plugins");
}
function AutomationOff({ actions }) {
- return (0, import_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
{ className: "bt-set-card bt-task-off" },
- (0, import_react20.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react20.createElement)(ClockIcon)),
- (0, import_react20.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
- (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
- (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (0, import_react21.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react21.createElement)(ClockIcon)),
+ (0, import_react21.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
+ (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
+ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
);
}
function SchedulesPage({ roster, actions, start }) {
const view = useSchedules(actions);
- const [adding, setAdding] = (0, import_react20.useState)(Boolean(start));
- const [editing, setEditing] = (0, import_react20.useState)(null);
- if (view === null) return (0, import_react20.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
- if (!view.available) return (0, import_react20.createElement)(AutomationOff, { actions });
+ const [adding, setAdding] = (0, import_react21.useState)(Boolean(start));
+ const [editing, setEditing] = (0, import_react21.useState)(null);
+ if (view === null) return (0, import_react21.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
+ if (!view.available) return (0, import_react21.createElement)(AutomationOff, { actions });
const shown = (next) => {
publish(next);
setAdding(false);
@@ -10865,51 +10910,51 @@ function SchedulesPage({ roster, actions, start }) {
setEditing(task);
};
const byChat = [
- ...roster.bots.map((bot) => [bot.id, (0, import_react20.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_react20.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_react21.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_react21.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_react20.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react20.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
+ adding ? (0, import_react21.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react21.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
setEditing(null);
setAdding(true);
- } }, (0, import_react20.createElement)(PlusIcon), t("New task"))),
- byChat.length === 0 && !adding ? (0, import_react20.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react20.createElement)(
+ } }, (0, import_react21.createElement)(PlusIcon), t("New task"))),
+ byChat.length === 0 && !adding ? (0, import_react21.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react21.createElement)(
"section",
{ key: id, className: "bt-set-section" },
- (0, import_react20.createElement)("h3", { className: "bt-task-head" }, avatar, name),
- (0, import_react20.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
+ (0, import_react21.createElement)("h3", { className: "bt-task-head" }, avatar, name),
+ (0, import_react21.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_react20.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
- return (0, import_react20.createElement)(
- import_react20.Fragment,
+ const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react21.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
+ return (0, import_react21.createElement)(
+ import_react21.Fragment,
null,
- open ? (0, import_react20.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
- rows.length + extra.length > 0 ? (0, import_react20.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
+ open ? (0, import_react21.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
+ rows.length + extra.length > 0 ? (0, import_react21.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
);
}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
- const [editing, setEditing] = (0, import_react20.useState)(null);
+ const [editing, setEditing] = (0, import_react21.useState)(null);
if (view === null) return null;
const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : [];
- return (0, import_react20.createElement)(
+ return (0, import_react21.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react20.createElement)("h3", null, t("Scheduled tasks")),
- !view.available ? (0, import_react20.createElement)(
+ (0, import_react21.createElement)("h3", null, t("Scheduled tasks")),
+ !view.available ? (0, import_react21.createElement)(
"div",
{ className: "bt-set-card" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
- (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
+ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
)
- ) : (0, import_react20.createElement)(TaskList, {
+ ) : (0, import_react21.createElement)(TaskList, {
tasks,
editing,
roster,
@@ -10920,23 +10965,23 @@ function BotSchedules({ bot, roster, actions }) {
setEditing(null);
},
onCancel: () => setEditing(null),
- extra: [(0, import_react20.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react20.createElement)(PlusIcon), t("New task"))]
+ extra: [(0, import_react21.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react21.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_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
null,
- (0, import_react20.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
- tasks.length === 0 ? (0, import_react20.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_react20.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react20.createElement)(
+ (0, import_react21.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
+ tasks.length === 0 ? (0, import_react21.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_react21.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react21.createElement)(
"button",
{ key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") },
- (0, import_react20.createElement)(ClockIcon),
- (0, import_react20.createElement)("span", { className: "bt-task-line-title" }, task.title),
- (0, import_react20.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
+ (0, import_react21.createElement)(ClockIcon),
+ (0, import_react21.createElement)("span", { className: "bt-task-line-title" }, task.title),
+ (0, import_react21.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
)))
);
}
@@ -10986,7 +11031,7 @@ var SCHEDULE_CSS = `
`;
// src/client/capability-settings.js
-var import_react21 = require("react");
+var import_react22 = require("react");
var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] });
var parseMap = (text, label) => {
let value;
@@ -11000,11 +11045,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_react21.createElement)(
+ return (0, import_react22.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t(label))),
- (0, import_react21.createElement)(multiline ? "textarea" : "input", {
+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t(label))),
+ (0, import_react22.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
@@ -11016,7 +11061,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl
);
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react21.useState)("");
+ const [error, setError] = (0, import_react22.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
@@ -11047,95 +11092,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_react21.createElement)(
+ return (0, import_react22.createElement)(
"form",
{ className: "bt-connector-form", onSubmit: submit },
- (0, import_react21.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
+ (0, import_react22.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
+ (0, import_react22.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_react21.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
+ ["mcp", "skill", "pack"].map((kind) => (0, import_react22.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
)
),
- (0, import_react21.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
- draft.kind === "mcp" ? (0, import_react21.createElement)(
+ (0, import_react22.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
+ draft.kind === "mcp" ? (0, import_react22.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
+ (0, import_react22.createElement)(
"select",
{ className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) },
- (0, import_react21.createElement)("option", { value: "stdio" }, t("Local command")),
- (0, import_react21.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
+ (0, import_react22.createElement)("option", { value: "stdio" }, t("Local command")),
+ (0, import_react22.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
)
) : null,
- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react21.createElement)(
+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react22.createElement)(
"div",
null,
- (0, import_react21.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
- (0, import_react21.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
- (0, import_react21.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_react22.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
+ (0, import_react22.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
+ (0, import_react22.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_react21.createElement)(
+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react22.createElement)(
"div",
null,
- (0, import_react21.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
- (0, import_react21.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_react22.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
+ (0, import_react22.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_react21.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
- draft.kind === "pack" ? (0, import_react21.createElement)(
+ draft.kind === "skill" ? (0, import_react22.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
+ draft.kind === "pack" ? (0, import_react22.createElement)(
"div",
{ className: "bt-set-card" },
- available.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react21.createElement)(
+ available.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react22.createElement)(
"label",
{ key: item.name, className: "bt-set-row bt-bs-row" },
- (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, item.name)),
- (0, import_react21.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_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, item.name)),
+ (0, import_react22.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_react21.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
- (0, import_react21.createElement)(
+ error ? (0, import_react22.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
+ (0, import_react22.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
- (0, import_react21.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
+ (0, import_react22.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_react21.createElement)(
+ return (0, import_react22.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react21.createElement)("span", { className: "bt-connector-name" }, item.name),
- (0, import_react21.createElement)("span", { className: "bt-connector-status" }, secondary)
+ (0, import_react22.createElement)("span", { className: "bt-connector-name" }, item.name),
+ (0, import_react22.createElement)("span", { className: "bt-connector-status" }, secondary)
),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)(
"span",
{ className: "bt-connector-actions" },
- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
- (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
- (0, import_react21.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_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
+ (0, import_react22.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_react21.useState)(null);
- const [confirming, setConfirming] = (0, import_react21.useState)("");
- (0, import_react21.useEffect)(() => {
+ const [draft, setDraft] = (0, import_react22.useState)(null);
+ const [confirming, setConfirming] = (0, import_react22.useState)("");
+ (0, import_react22.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
@@ -11158,20 +11203,20 @@ function CapabilitiesPage({ roster, actions, run }) {
setConfirming("");
void run(actions.deleteCapability(item.name));
};
- return (0, import_react21.createElement)(
+ return (0, import_react22.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react21.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)("h3", null, t("Shared capabilities")),
+ (0, import_react22.createElement)(
"div",
{ className: "bt-actions" },
- (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
+ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
),
- draft ? (0, import_react21.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
- (0, import_react21.createElement)(
+ draft ? (0, import_react22.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
+ (0, import_react22.createElement)(
"div",
{ className: "bt-set-card bt-connectors-card" },
- items.length === 0 ? (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react21.createElement)(CapabilityRow, {
+ items.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react22.createElement)(CapabilityRow, {
key: item.name,
item,
confirming: confirming === item.name,
@@ -11187,10 +11232,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_react21.createElement)(
+ return (0, import_react22.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react21.createElement)(SwitchRow2, {
+ items.map((item) => (0, import_react22.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
@@ -11201,7 +11246,7 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
}
// src/client/connector-bots.js
-var import_react22 = require("react");
+var import_react23 = 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));
@@ -11217,37 +11262,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_react22.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_react23.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_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-connector-bots" },
- bots.length === 0 ? (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react22.createElement)(
+ bots.length === 0 ? (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react23.createElement)(
"div",
{ key: bot.id, className: "bt-connector-bot" },
- (0, import_react22.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react23.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
)),
- (0, import_react22.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_react23.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_react22.createElement)(
+ return (0, import_react23.createElement)(
Card2,
{ title: t("Connectors") },
- connected.map((connector) => (0, import_react22.createElement)(
+ connected.map((connector) => (0, import_react23.createElement)(
"div",
{ key: connector.id, className: "bt-connector-bot" },
- (0, import_react22.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react23.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
))
);
}
// src/client/memory-panel.js
-var import_react23 = require("react");
+var import_react24 = require("react");
var POLL_MS5 = 1500;
var MENU_EXIT_MS2 = 120;
var counts = /* @__PURE__ */ new Map();
@@ -11258,8 +11303,8 @@ function noteCount(botId, count) {
for (const listener of countListeners) listener();
}
function useMemoryCount(botId, actions) {
- const [count, setCount] = (0, import_react23.useState)(counts.get(botId) ?? null);
- (0, import_react23.useEffect)(() => {
+ const [count, setCount] = (0, import_react24.useState)(counts.get(botId) ?? null);
+ (0, import_react24.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -11278,16 +11323,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_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
null,
- (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Memory")),
+ (0, import_react24.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_react23.createElement)(MemoryIcon),
- (0, import_react23.createElement)("span", { className: "bt-mem-row-copy" }, label),
- (0, import_react23.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react23.createElement)(ChevronRightIcon))
+ (0, import_react24.createElement)(MemoryIcon),
+ (0, import_react24.createElement)("span", { className: "bt-mem-row-copy" }, label),
+ (0, import_react24.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react24.createElement)(ChevronRightIcon))
)
);
}
@@ -11302,45 +11347,45 @@ function ago(at, now) {
return format.format(-Math.round(hours / 24), "day");
}
function Skeleton({ label }) {
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react23.createElement)(
+ [0, 1].map((block) => (0, import_react24.createElement)(
"div",
{ key: block, className: "bt-mem-skel-block" },
- (0, import_react23.createElement)("span", { className: "bt-mem-skel-head" }),
- (0, import_react23.createElement)("span", null),
- (0, import_react23.createElement)("span", null),
- (0, import_react23.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
+ (0, import_react24.createElement)("span", { className: "bt-mem-skel-head" }),
+ (0, import_react24.createElement)("span", null),
+ (0, import_react24.createElement)("span", null),
+ (0, import_react24.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
)),
- label ? (0, import_react23.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
+ label ? (0, import_react24.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
);
}
function Summary({ bot, view, regenerate }) {
if (view.entries.length === 0) {
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-empty" },
- (0, import_react23.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react23.createElement)(MemoryIcon)),
- (0, import_react23.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
- (0, import_react23.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_react24.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react24.createElement)(MemoryIcon)),
+ (0, import_react24.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
+ (0, import_react24.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_react23.createElement)(
+ const failed = view.error && !view.summarizing ? (0, import_react24.createElement)(
"div",
{ className: "bt-mem-failed", role: "alert" },
- (0, import_react23.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_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
+ (0, import_react24.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
) : null;
- if (!view.summary) return (0, import_react23.createElement)(import_react23.Fragment, null, failed, failed ? null : (0, import_react23.createElement)(Skeleton, { label: t("Writing the summary…") }));
- return (0, import_react23.createElement)(
- import_react23.Fragment,
+ if (!view.summary) return (0, import_react24.createElement)(import_react24.Fragment, null, failed, failed ? null : (0, import_react24.createElement)(Skeleton, { label: t("Writing the summary…") }));
+ return (0, import_react24.createElement)(
+ import_react24.Fragment,
null,
failed,
- (0, import_react23.createElement)(
+ (0, import_react24.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_react23.createElement)("h3", { key: index }, block.text) : (0, import_react23.createElement)("p", { key: index }, block.text))
+ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react24.createElement)("h3", { key: index }, block.text) : (0, import_react24.createElement)("p", { key: index }, block.text))
)
);
}
@@ -11352,9 +11397,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => {
return groups;
}, []);
function EntryRow({ entry, remove }) {
- const [confirming, setConfirming] = (0, import_react23.useState)(false);
- const [busy, setBusy] = (0, import_react23.useState)(false);
- (0, import_react23.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react24.useState)(false);
+ const [busy, setBusy] = (0, import_react24.useState)(false);
+ (0, import_react24.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -11369,16 +11414,16 @@ function EntryRow({ entry, remove }) {
setBusy(true);
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-entry-copy" },
- (0, import_react23.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
- meta ? (0, import_react23.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
+ (0, import_react24.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
+ meta ? (0, import_react24.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
),
- (0, import_react23.createElement)("button", {
+ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
@@ -11386,7 +11431,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_react23.createElement)(TrashIcon))
+ }, confirming ? t("Remove") : (0, import_react24.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
@@ -11395,25 +11440,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_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react23.createElement)(
+ sections.map((section) => (0, import_react24.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-section-head" },
- (0, import_react23.createElement)("h3", null, section.title),
- section.hint ? (0, import_react23.createElement)("span", null, section.hint) : null,
- (0, import_react23.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_react24.createElement)("h3", null, section.title),
+ section.hint ? (0, import_react24.createElement)("span", null, section.hint) : null,
+ (0, import_react24.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_react23.createElement)(
- import_react23.Fragment,
+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react24.createElement)(
+ import_react24.Fragment,
{ key: group.key },
- group.topic ? (0, import_react23.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
- (0, import_react23.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react23.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
- )) : (0, import_react23.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
+ group.topic ? (0, import_react24.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
+ (0, import_react24.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react24.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
+ )) : (0, import_react24.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
))
);
}
@@ -11425,40 +11470,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_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-reply-head" },
- (0, import_react23.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
- answer.pending ? null : (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react23.createElement)(CloseIcon))
+ (0, import_react24.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
+ answer.pending ? null : (0, import_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react24.createElement)(CloseIcon))
),
- answer.pending ? (0, import_react23.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react23.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react23.createElement)(
- import_react23.Fragment,
+ answer.pending ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react24.createElement)(
+ import_react24.Fragment,
null,
- answer.reply ? (0, import_react23.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
- answer.changes.length > 0 ? (0, import_react23.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react23.createElement)(
+ answer.reply ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
+ answer.changes.length > 0 ? (0, import_react24.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react24.createElement)(
"li",
{ key: index, "data-ok": change.ok || void 0 },
- change.ok ? (0, import_react23.createElement)(CheckIcon) : (0, import_react23.createElement)(CloseIcon),
- (0, import_react23.createElement)("span", null, changeLine(change))
+ change.ok ? (0, import_react24.createElement)(CheckIcon) : (0, import_react24.createElement)(CloseIcon),
+ (0, import_react24.createElement)("span", null, changeLine(change))
))) : null
)
);
}
function useMemoryView(botId, actions) {
- const [view, setView] = (0, import_react23.useState)(null);
- const [error, setError] = (0, import_react23.useState)("");
- const timer = (0, import_react23.useRef)(0);
- const latest = (0, import_react23.useRef)(0);
- const alive = (0, import_react23.useRef)(true);
- const show = (0, import_react23.useCallback)((next) => {
+ const [view, setView] = (0, import_react24.useState)(null);
+ const [error, setError] = (0, import_react24.useState)("");
+ const timer = (0, import_react24.useRef)(0);
+ const latest = (0, import_react24.useRef)(0);
+ const alive = (0, import_react24.useRef)(true);
+ const show = (0, import_react24.useCallback)((next) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react23.useCallback)(async (flags = {}) => {
+ const load = (0, import_react24.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
@@ -11470,7 +11515,7 @@ function useMemoryView(botId, actions) {
if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure));
}
}, [botId]);
- (0, import_react23.useEffect)(() => {
+ (0, import_react24.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
@@ -11481,8 +11526,8 @@ function useMemoryView(botId, actions) {
return { view, error, load, show };
}
function useNow3(every) {
- const [now, setNow] = (0, import_react23.useState)(Date.now());
- (0, import_react23.useEffect)(() => {
+ const [now, setNow] = (0, import_react24.useState)(Date.now());
+ (0, import_react24.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
@@ -11490,18 +11535,18 @@ function useNow3(every) {
}
function MemoryDialog({ bot, actions, leaving = false }) {
const { view, error, load, show } = useMemoryView(bot.id, actions);
- const [page, setPage] = (0, import_react23.useState)("summary");
- const [menu, setMenu] = (0, import_react23.useState)(false);
+ const [page, setPage] = (0, import_react24.useState)("summary");
+ const [menu, setMenu] = (0, import_react24.useState)(false);
const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
- const [text, setText] = (0, import_react23.useState)("");
- const [answer, setAnswer] = (0, import_react23.useState)(null);
- const menuRef = (0, import_react23.useRef)(null);
- const dotsRef = (0, import_react23.useRef)(null);
- const inputRef = (0, import_react23.useRef)(null);
+ const [text, setText] = (0, import_react24.useState)("");
+ const [answer, setAnswer] = (0, import_react24.useState)(null);
+ const menuRef = (0, import_react24.useRef)(null);
+ const dotsRef = (0, import_react24.useRef)(null);
+ const inputRef = (0, import_react24.useRef)(null);
const now = useNow3(3e4);
- const close = (0, import_react23.useCallback)(() => actions.closeMemory(), []);
+ const close = (0, import_react24.useCallback)(() => actions.closeMemory(), []);
useEscape(close);
- (0, import_react23.useEffect)(() => {
+ (0, import_react24.useEffect)(() => {
if (!menu) return void 0;
const onDown = (event) => {
if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
@@ -11563,52 +11608,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_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-layer", "data-leaving": leaving || void 0 },
- (0, import_react23.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-head" },
- page === "entries" ? (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react23.createElement)(ChevronLeftIcon)) : null,
- (0, import_react23.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
- (0, import_react23.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
- (0, import_react23.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react23.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_react23.createElement)(DotsIcon)),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react23.createElement)(CloseIcon)),
- menuShown ? (0, import_react23.createElement)(
+ page === "entries" ? (0, import_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react24.createElement)(ChevronLeftIcon)) : null,
+ (0, import_react24.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
+ (0, import_react24.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
+ (0, import_react24.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react24.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_react24.createElement)(DotsIcon)),
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react24.createElement)(CloseIcon)),
+ menuShown ? (0, import_react24.createElement)(
"div",
{ ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"button",
{ type: "button", role: "menuitem", onClick: openEntries },
- (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
- (0, import_react23.createElement)("span", { className: "bt-menu-hint" }, String(count))
+ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
+ (0, import_react24.createElement)("span", { className: "bt-menu-hint" }, String(count))
),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"button",
{ type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate },
- (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
+ (0, import_react24.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
)
) : null
),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-body" },
- error ? (0, import_react23.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
- view === null ? error ? null : (0, import_react23.createElement)(Skeleton) : page === "entries" ? (0, import_react23.createElement)(Entries, { bot, view, remove }) : (0, import_react23.createElement)(Summary, { bot, view, regenerate })
+ error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
+ view === null ? error ? null : (0, import_react24.createElement)(Skeleton) : page === "entries" ? (0, import_react24.createElement)(Entries, { bot, view, remove }) : (0, import_react24.createElement)(Summary, { bot, view, regenerate })
),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-mem-foot" },
- answer ? (0, import_react23.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
- (0, import_react23.createElement)(
+ answer ? (0, import_react24.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
+ (0, import_react24.createElement)(
"form",
{ className: "bt-mem-ask", onSubmit: ask },
- (0, import_react23.createElement)("input", {
+ (0, import_react24.createElement)("input", {
ref: inputRef,
className: "bt-mem-input",
value: text,
@@ -11618,10 +11663,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_react23.createElement)(
+ (0, import_react24.createElement)(
"button",
{ type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") },
- busy ? (0, import_react23.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react23.createElement)(ArrowUpIcon)
+ busy ? (0, import_react24.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react24.createElement)(ArrowUpIcon)
)
)
)
@@ -11630,7 +11675,7 @@ function MemoryDialog({ bot, actions, leaving = false }) {
}
// src/client/standing-approvals.js
-var import_react24 = require("react");
+var import_react25 = require("react");
var standingOf = (target, roster) => {
const me = roster?.self?.memberId;
return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -11639,19 +11684,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_react24.createElement)(
+ return (0, import_react25.createElement)(
Card2,
{ title: t("Always allowed") },
- grants.map((grant) => (0, import_react24.createElement)(
+ grants.map((grant) => (0, import_react25.createElement)(
"div",
{ key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react24.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
- (0, import_react24.createElement)("span", { className: "bt-set-hint" }, hint)
+ (0, import_react25.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint)
),
- (0, import_react24.createElement)("button", {
+ (0, import_react25.createElement)("button", {
type: "button",
className: "bt-soft bt-soft-sm",
"aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -11662,7 +11707,7 @@ function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card
}
// src/client/bot-settings.js
-var import_react25 = require("react");
+var import_react26 = 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"));
@@ -11686,8 +11731,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_react25.useState)(null);
- const reverting = (0, import_react25.useRef)(false);
+ const [draft, setDraft] = (0, import_react26.useState)(null);
+ const reverting = (0, import_react26.useRef)(false);
const commit = () => {
const next = draft?.trim();
setDraft(null);
@@ -11698,7 +11743,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_react25.createElement)(multiline ? "textarea" : "input", {
+ return (0, import_react26.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
value: draft ?? value ?? "",
placeholder,
@@ -11719,29 +11764,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu
});
}
function Row({ label, hint, stack = false, children }) {
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"label",
{ className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) : null),
children
);
}
function Card({ title, children }) {
- return (0, import_react25.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react25.createElement)("h3", null, title) : null, (0, import_react25.createElement)("div", { className: "bt-set-card" }, children));
+ return (0, import_react26.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react26.createElement)("h3", null, title) : null, (0, import_react26.createElement)("div", { className: "bt-set-card" }, children));
}
function Switch({ on, label, disabled, onChange }) {
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"button",
{ type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) },
- (0, import_react25.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
+ (0, import_react26.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
);
}
function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) {
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- (0, import_react25.createElement)(Switch, { on, label, disabled, onChange })
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ (0, import_react26.createElement)(Switch, { on, label, disabled, onChange })
);
}
function modelHint(bot, roster) {
@@ -11752,22 +11797,22 @@ function modelHint(bot, roster) {
return t("Applies from the next message.");
}
function ShapeRing({ shape, size }) {
- const maskId = (0, import_react25.useRef)("");
+ const maskId = (0, import_react26.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_react25.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } });
+ return (0, import_react26.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_react25.createElement)(
- import_react25.Fragment,
+ return (0, import_react26.createElement)(
+ import_react26.Fragment,
null,
- (0, import_react25.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react25.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react25.createElement)("button", {
+ (0, import_react26.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react26.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react26.createElement)("button", {
key: id,
type: "button",
className: "bt-shape",
@@ -11775,8 +11820,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(shapeOf(id).label),
"aria-pressed": !hasPhoto && look.shape === id,
onClick: () => onShape(id)
- }, (0, import_react25.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react25.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
- (0, import_react25.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react25.createElement)("button", {
+ }, (0, import_react26.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react26.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
+ (0, import_react26.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react26.createElement)("button", {
key: color,
type: "button",
className: "bt-color",
@@ -11784,19 +11829,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(capitalize(color)),
"aria-pressed": look.color === color,
onClick: () => onColor(color)
- }, (0, import_react25.createElement)("span", { style: { background: paintCss(color) } }))))
+ }, (0, import_react26.createElement)("span", { style: { background: paintCss(color) } }))))
);
}
function AvatarEditor({ bot, actions, run, onClose }) {
const look = lookOf(bot);
- const [tab, setTab] = (0, import_react25.useState)(look.image ? "upload" : "bot");
- const [over, setOver] = (0, import_react25.useState)(false);
- const ref = (0, import_react25.useRef)(null);
- const fileRef = (0, import_react25.useRef)(null);
+ const [tab, setTab] = (0, import_react26.useState)(look.image ? "upload" : "bot");
+ const [over, setOver] = (0, import_react26.useState)(false);
+ const ref = (0, import_react26.useRef)(null);
+ const fileRef = (0, import_react26.useRef)(null);
const upload = (file) => {
if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } })));
};
- (0, import_react25.useEffect)(() => {
+ (0, import_react26.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
onClose();
@@ -11814,7 +11859,7 @@ function AvatarEditor({ bot, actions, run, onClose }) {
window.removeEventListener("keydown", onKey, true);
};
}, [onClose]);
- (0, import_react25.useEffect)(() => {
+ (0, import_react26.useEffect)(() => {
if (tab !== "upload") return void 0;
const onPaste = (event) => {
const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
@@ -11828,32 +11873,32 @@ function AvatarEditor({ bot, actions, run, onClose }) {
}, [tab, bot]);
const hasPhoto = Boolean(look.image);
const glide = useGlide(tab);
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-avedit-top" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" },
- (0, import_react25.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react25.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
+ (0, import_react26.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react26.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
),
- (0, import_react25.createElement)("button", {
+ (0, import_react26.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_react25.createElement)(CharacterGrid, {
+ tab === "bot" ? (0, import_react26.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_react25.createElement)(
+ tab === "upload" ? (0, import_react26.createElement)(
"div",
{
className: "bt-dropzone",
@@ -11869,15 +11914,15 @@ function AvatarEditor({ bot, actions, run, onClose }) {
upload(event.dataTransfer.files?.[0]);
}
},
- hasPhoto ? (0, import_react25.createElement)("img", { src: look.image, alt: "" }) : null,
- (0, import_react25.createElement)("div", null, t("Drag, drop, or paste an image")),
- (0, import_react25.createElement)(
+ hasPhoto ? (0, import_react26.createElement)("img", { src: look.image, alt: "" }) : null,
+ (0, import_react26.createElement)("div", null, t("Drag, drop, or paste an image")),
+ (0, import_react26.createElement)(
"div",
{ className: "bt-actions", style: { justifyContent: "center" } },
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
- hasPhoto ? (0, import_react25.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_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
+ hasPhoto ? (0, import_react26.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_react25.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (0, import_react26.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);
@@ -11888,22 +11933,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_react25.createElement)(
+ return (0, import_react26.createElement)(
Card,
{ title: t("Memory") },
- (0, import_react25.createElement)(
+ (0, import_react26.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label)),
- (0, import_react25.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react25.createElement)(ChevronRightIcon))
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label)),
+ (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon))
)
);
}
function BotSettingsForm({ bot, roster, actions, run }) {
- const [editing, setEditing] = (0, import_react25.useState)(false);
- const [confirming, setConfirming] = (0, import_react25.useState)(false);
- const closeEditor = (0, import_react25.useCallback)(() => setEditing(false), []);
- (0, import_react25.useEffect)(() => {
+ const [editing, setEditing] = (0, import_react26.useState)(false);
+ const [confirming, setConfirming] = (0, import_react26.useState)(false);
+ const closeEditor = (0, import_react26.useCallback)(() => setEditing(false), []);
+ (0, import_react26.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -11918,116 +11963,116 @@ function BotSettingsForm({ bot, roster, actions, run }) {
if (confirming) void run(actions.deleteBot(bot.id));
else setConfirming(true);
};
- (0, import_react25.useEffect)(() => {
+ (0, import_react26.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-avatar-anchor" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"button",
{ type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) },
- (0, import_react25.createElement)(BotMark, { bot, size: 64, live: true }),
- (0, import_react25.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react25.createElement)(PencilIcon))
+ (0, import_react26.createElement)(BotMark, { bot, size: 64, live: true }),
+ (0, import_react26.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react26.createElement)(PencilIcon))
),
- editing ? (0, import_react25.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
+ editing ? (0, import_react26.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react25.createElement)("span", { className: "bt-bs-name" }, bot.name),
- (0, import_react25.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
+ (0, import_react26.createElement)("span", { className: "bt-bs-name" }, bot.name),
+ (0, import_react26.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
)
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react25.createElement)(Row, { label: t("Name") }, (0, import_react25.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
- (0, import_react25.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react25.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
+ (0, import_react26.createElement)(Row, { label: t("Name") }, (0, import_react26.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
+ (0, import_react26.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react26.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Model") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
- (0, import_react25.createElement)(ModelMenu, { bot, roster, actions, run })
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
+ (0, import_react26.createElement)(ModelMenu, { bot, roster, actions, run })
),
- bot.model ? (0, import_react25.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
+ bot.model ? (0, import_react26.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react25.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
+ (0, import_react26.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
)
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Instructions") },
- (0, import_react25.createElement)(
+ (0, import_react26.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_react25.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
+ (0, import_react26.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_react25.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react25.createElement)(Card, { title: t("Sharing") }, children) }),
- (0, import_react25.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
- (0, import_react25.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
- roster.memory === false ? null : (0, import_react25.createElement)(MemoryCard, { bot, actions }),
- (0, import_react25.createElement)(BotSchedules, { bot, roster, actions }),
- (0, import_react25.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react26.createElement)(Card, { title: t("Sharing") }, children) }),
+ (0, import_react26.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
+ (0, import_react26.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
+ roster.memory === false ? null : (0, import_react26.createElement)(MemoryCard, { bot, actions }),
+ (0, import_react26.createElement)(BotSchedules, { bot, roster, actions }),
+ (0, import_react26.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
+ (0, import_react26.createElement)(
Card,
{ title: t("Main Bot") },
- onlyMain ? (0, import_react25.createElement)(
+ onlyMain ? (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react25.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_react26.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_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
- ) : (0, import_react25.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_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
+ ) : (0, import_react26.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_react25.createElement)(
+ isTile ? null : (0, import_react26.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react25.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_react25.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_react26.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_react26.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_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("More") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
- (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
),
- isMainBot ? null : (0, import_react25.createElement)(
+ isMainBot ? null : (0, import_react26.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react25.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
- (0, import_react25.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_react26.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
+ (0, import_react26.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_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
)
)
);
@@ -12068,7 +12113,7 @@ ${MODEL_TEST_CSS}
`;
// src/client/group-settings.js
-var import_react26 = require("react");
+var import_react27 = 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."]
@@ -12100,9 +12145,9 @@ function faceMarks(person, roster, size) {
for (const seat of person?.seats ?? []) {
const bot = faceSeatBot(seat, roster);
if (!bot) continue;
- marks.push((0, import_react26.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
+ marks.push((0, import_react27.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
}
- if (marks.length === 0) return [(0, import_react26.createElement)(PersonMark, { key: "person", size, name: person?.name })];
+ if (marks.length === 0) return [(0, import_react27.createElement)(PersonMark, { key: "person", size, name: person?.name })];
return marks;
}
function faceRoomCount(room2, roster) {
@@ -12116,31 +12161,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_react26.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_react26.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_react26.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
- return index === 0 ? marked : [(0, import_react26.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
+ const name = seat.local && onOpenBot ? (0, import_react27.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_react27.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_react27.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
+ return index === 0 ? marked : [(0, import_react27.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
});
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"span",
{ className: "bt-gs-who bt-face-person" },
- (0, import_react26.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
+ (0, import_react27.createElement)(
"span",
{ className: "bt-face-person-text" },
- (0, import_react26.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
- (0, import_react26.createElement)("span", { className: "bt-face-bots" }, bots)
+ (0, import_react27.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
+ (0, import_react27.createElement)("span", { className: "bt-face-bots" }, bots)
)
)
);
}
function FaceLead({ room: room2, roster, pick: pick2 }) {
const lead = room2.face?.lead;
- const [open, setOpen] = (0, import_react26.useState)(false);
- const [busy, setBusy] = (0, import_react26.useState)(false);
- const [error, setError] = (0, import_react26.useState)("");
+ const [open, setOpen] = (0, import_react27.useState)(false);
+ const [busy, setBusy] = (0, import_react27.useState)(false);
+ const [error, setError] = (0, import_react27.useState)("");
const seats = faceSeats(room2, roster);
const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
if (!current) return null;
@@ -12157,30 +12202,30 @@ function FaceLead({ room: room2, roster, pick: pick2 }) {
setBusy(false);
}
};
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-face-lead", "data-flybot": "face-lead" },
- (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
+ (0, import_react27.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react26.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
- (0, import_react26.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_react27.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
+ (0, import_react27.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_react26.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
+ canPick ? (0, import_react27.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_react26.createElement)(
+ open && canPick ? seats.map((seat) => (0, import_react27.createElement)(
"div",
{ key: seat.key, className: "bt-set-row" },
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
- seat === current ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
+ seat === current ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react27.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_react26.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_react26.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
- error ? (0, import_react26.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
+ open && canPick && lead.picked ? (0, import_react27.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_react27.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
+ error ? (0, import_react27.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
);
}
function seatedCount(room2) {
@@ -12196,9 +12241,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_react26.useState)(false);
- const [confirming, setConfirming] = (0, import_react26.useState)(false);
- (0, import_react26.useEffect)(() => {
+ const [adding, setAdding] = (0, import_react27.useState)(false);
+ const [confirming, setConfirming] = (0, import_react27.useState)(false);
+ (0, import_react27.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -12236,41 +12281,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (done) onDeleted?.();
});
};
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
+ (0, import_react27.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react26.createElement)("span", { className: "bt-bs-name" }, room2.name),
- (0, import_react26.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ (0, import_react27.createElement)("span", { className: "bt-bs-name" }, room2.name),
+ (0, import_react27.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
)
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
Row,
{ label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
- (0, import_react26.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
+ (0, import_react27.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
Row,
{ label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
- (0, import_react26.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_react27.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_react26.createElement)(
+ (0, import_react27.createElement)(
Card,
{ title: t("Who answers") },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react26.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react26.createElement)("button", {
+ (0, import_react27.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react27.createElement)("button", {
key: id,
type: "button",
"aria-pressed": mode === id,
@@ -12280,104 +12325,104 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (mode !== id) void update({ mode: id });
}
}, t(label)))),
- (0, import_react26.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ (0, import_react27.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
)
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react26.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ 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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
)
),
- face ? (0, import_react26.createElement)(
+ face ? (0, import_react27.createElement)(
Card,
{ title: t("Members · {count}", { count: people2.length }) },
- people2.map((person) => (0, import_react26.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ people2.map((person) => (0, import_react27.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
) : null,
- face ? null : (0, import_react26.createElement)(
+ face ? null : (0, import_react27.createElement)(
Card,
{ title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
- humans.map((person) => (0, import_react26.createElement)(
+ humans.map((person) => (0, import_react27.createElement)(
"div",
{ key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react26.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
- (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Human")),
- person.role === "admin" ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
+ (0, import_react27.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
+ (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Human")),
+ person.role === "admin" ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
),
- humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react26.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_react27.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;
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"button",
{ type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
- (0, import_react26.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
- (0, import_react26.createElement)("span", { className: "bt-row-title" }, member.name),
- leads ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
- (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
+ (0, import_react27.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
+ (0, import_react27.createElement)("span", { className: "bt-row-title" }, member.name),
+ leads ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
),
- leads ? (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
- members.length > 2 ? (0, import_react26.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_react27.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
+ members.length > 2 ? (0, import_react27.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_react26.createElement)(
+ remotes.map((person) => (0, import_react27.createElement)(
"div",
{ key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react26.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react26.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
+ (0, import_react27.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
+ (0, import_react27.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_react26.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_react27.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_react26.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_react27.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_react26.createElement)(
+ ...outsidePeople.map((person) => (0, import_react27.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
+ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
)),
- ...outside.map((bot) => (0, import_react26.createElement)(
+ ...outside.map((bot) => (0, import_react27.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_react26.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, bot.name)),
- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add"))
+ (0, import_react27.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, bot.name)),
+ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add"))
)),
- ...outsideRemotes.map((bot) => (0, import_react26.createElement)(
+ ...outsideRemotes.map((bot) => (0, import_react27.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_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
- (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
+ (0, import_react27.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_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
+ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
)),
- (0, import_react26.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_react26.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react26.createElement)(PlusIcon), t("Add member")) : null,
- (0, import_react26.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react26.createElement)(
+ (0, import_react27.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_react27.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react27.createElement)(PlusIcon), t("Add member")) : null,
+ (0, import_react27.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react27.createElement)(
"span",
{ className: "bt-set-hint" },
// The host checks for two members on every change, even a rename.
@@ -12385,37 +12430,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_react26.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react27.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
),
- (0, import_react26.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
+ (0, import_react27.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react26.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_react26.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_react27.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_react27.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_react26.createElement)(
+ (0, import_react27.createElement)(
Card,
{ title: t("More") },
- (0, import_react26.createElement)(
+ (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_react26.createElement)(
+ (0, import_react27.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react26.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
- (0, import_react26.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_react27.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
+ (0, import_react27.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_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
+ (0, import_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon))
)
)
);
}
function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
- const [admin, setAdmin] = (0, import_react26.useState)(null);
- const [members, setMembers] = (0, import_react26.useState)([]);
- const [name, setName] = (0, import_react26.useState)("");
- const [busy, setBusy] = (0, import_react26.useState)(false);
+ const [admin, setAdmin] = (0, import_react27.useState)(null);
+ const [members, setMembers] = (0, import_react27.useState)([]);
+ const [name, setName] = (0, import_react27.useState)("");
+ const [busy, setBusy] = (0, import_react27.useState)(false);
const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
const pickAdmin = (id) => {
setAdmin(id);
@@ -12431,48 +12476,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
if (done && room2) onCreated(room2);
else setBusy(false);
};
- const chip = (bot, on, onClick, role) => (0, import_react26.createElement)("button", {
+ const chip = (bot, on, onClick, role) => (0, import_react27.createElement)("button", {
key: bot.id,
type: "button",
role,
className: "bt-ng-chip",
"aria-checked": on,
onClick
- }, (0, import_react26.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react26.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react26.createElement)(AdminStar) : null);
- return (0, import_react26.createElement)(
+ }, (0, import_react27.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react27.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react27.createElement)(AdminStar) : null);
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-set-card bt-ng" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react26.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react27.createElement)(AdminStar), t("Choose the group admin"))),
+ (0, import_react27.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_react26.createElement)(
+ admin === null ? null : (0, import_react27.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
+ (0, import_react27.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_react26.createElement)(
+ admin === null || members.length === 0 ? null : (0, import_react27.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-ng-step" },
- (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react26.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
+ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
+ (0, import_react27.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-set-row bt-task-foot" },
- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react26.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_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ (0, import_react27.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
)
);
}
@@ -12508,13 +12553,13 @@ button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-unde
`;
// src/client/palette.js
-var import_react27 = require("react");
+var import_react28 = 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_react27.useState)(start ?? "");
- const [cursor, setCursor] = (0, import_react27.useState)(0);
- const [leaving, setLeaving] = (0, import_react27.useState)(false);
+ const [query, setQuery] = (0, import_react28.useState)(start ?? "");
+ const [cursor, setCursor] = (0, import_react28.useState)(0);
+ const [leaving, setLeaving] = (0, import_react28.useState)(false);
const wanted = query.trim().toLowerCase();
const close = () => {
if (leaving) return;
@@ -12550,7 +12595,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_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
+ avatar: (0, import_react28.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,
@@ -12561,7 +12606,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
})),
...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
key: room2.id,
- avatar: (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: (0, import_react28.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,
@@ -12574,7 +12619,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_react27.createElement)("span", { className: "bt-palette-agent" }, (0, import_react27.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ avatar: (0, import_react28.createElement)("span", { className: "bt-palette-agent" }, (0, import_react28.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
title: "DSH Agent",
subtitle: t("Plain dsh session"),
run: () => actions.openAgent(agent.id)
@@ -12587,16 +12632,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
key: `${id}:${turn}`,
title: bot?.name ?? room2?.name ?? t("Bot"),
subtitle: snippet,
- avatar: bot ? (0, import_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: bot ? (0, import_react28.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react28.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_react27.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
- { key: "cmd:bot", icon: (0, import_react27.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
- { key: "cmd:group", icon: (0, import_react27.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
- { key: "cmd:settings", icon: (0, import_react27.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") }
+ { key: "cmd:new", icon: (0, import_react28.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
+ { key: "cmd:bot", icon: (0, import_react28.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
+ { key: "cmd:group", icon: (0, import_react28.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
+ { key: "cmd:settings", icon: (0, import_react28.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);
@@ -12606,7 +12651,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
${shown}`, { axis: "y", ...QUICK_EDGES });
const listRef = glide.box;
useFlip(listRef, shown, "[data-flip]");
- (0, import_react27.useEffect)(() => {
+ (0, import_react28.useEffect)(() => {
listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
}, [active, query]);
const choose = (row) => {
@@ -12615,18 +12660,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
close();
};
let index = -1;
- return (0, import_react27.createElement)(
+ return (0, import_react28.createElement)(
"div",
{ className: "bt-palette-layer", "data-leaving": leaving || void 0 },
- (0, import_react27.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
+ (0, import_react28.createElement)(
"div",
{ className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ className: "bt-palette-head" },
- (0, import_react27.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react27.createElement)(SearchIcon)),
- (0, import_react27.createElement)("input", {
+ (0, import_react28.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react28.createElement)(SearchIcon)),
+ (0, import_react28.createElement)("input", {
autoFocus: true,
value: query,
placeholder: t("Search Bots, chats and messages"),
@@ -12652,29 +12697,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- query ? (0, import_react27.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
+ query ? (0, import_react28.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
setQuery("");
setCursor(0);
- } }, (0, import_react27.createElement)(CloseIcon)) : null
+ } }, (0, import_react28.createElement)(CloseIcon)) : null
),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
- (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- rows.length === 0 ? (0, import_react27.createElement)(
+ (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ rows.length === 0 ? (0, import_react28.createElement)(
"div",
{ className: "bt-palette-empty" },
- (0, import_react27.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react27.createElement)(SearchIcon)),
- (0, import_react27.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
- (0, import_react27.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
- ) : sections.map(([title, items]) => (0, import_react27.createElement)(
- import_react27.Fragment,
+ (0, import_react28.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react28.createElement)(SearchIcon)),
+ (0, import_react28.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
+ (0, import_react28.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
+ ) : sections.map(([title, items]) => (0, import_react28.createElement)(
+ import_react28.Fragment,
{ key: title },
- (0, import_react27.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ (0, import_react28.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
items.map((row) => {
index += 1;
const at = index;
- return (0, import_react27.createElement)(
+ return (0, import_react28.createElement)(
"button",
{
key: row.key,
@@ -12688,14 +12733,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
},
onClick: () => choose(row)
},
- (0, import_react27.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react27.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react28.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
+ (0, import_react28.createElement)(
"span",
{ className: "bt-palette-text" },
- (0, import_react27.createElement)("span", { className: "bt-palette-title" }, row.title),
- row.subtitle ? (0, import_react27.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
+ (0, import_react28.createElement)("span", { className: "bt-palette-title" }, row.title),
+ row.subtitle ? (0, import_react28.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
),
- row.badge ? (0, import_react27.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
+ row.badge ? (0, import_react28.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
);
})
))
@@ -12737,7 +12782,7 @@ async function afterCreate({ id, text, actions }) {
}
// src/client/create-bot.js
-var import_react28 = require("react");
+var import_react29 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -12745,17 +12790,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_react28.useState)(() => randomLook());
- const [image, setImage] = (0, import_react28.useState)(null);
- const [name, setName] = (0, import_react28.useState)(start?.name ?? "");
- const [model, setModel] = (0, import_react28.useState)(() => startModel(roster));
- const [effort, setEffort] = (0, import_react28.useState)(void 0);
- const [busy, setBusy] = (0, import_react28.useState)(false);
- const [error, setError] = (0, import_react28.useState)("");
- const fileRef = (0, import_react28.useRef)(null);
- const frame = (0, import_react28.useRef)(null);
+ const [look, setLook] = (0, import_react29.useState)(() => randomLook());
+ const [image, setImage] = (0, import_react29.useState)(null);
+ const [name, setName] = (0, import_react29.useState)(start?.name ?? "");
+ const [model, setModel] = (0, import_react29.useState)(() => startModel(roster));
+ const [effort, setEffort] = (0, import_react29.useState)(void 0);
+ const [busy, setBusy] = (0, import_react29.useState)(false);
+ const [error, setError] = (0, import_react29.useState)("");
+ const fileRef = (0, import_react29.useRef)(null);
+ const frame = (0, import_react29.useRef)(null);
useEscape(onBack);
- (0, import_react28.useEffect)(() => {
+ (0, import_react29.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
@@ -12780,25 +12825,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_react28.createElement)(
+ const card = (0, import_react29.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"span",
{ key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" },
- (0, import_react28.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
+ (0, import_react29.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
),
- (0, import_react28.createElement)("span", { className: "bt-create-name" }, finalName)
+ (0, import_react29.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react28.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)("div", { className: "bt-create-step" }, t("Look")),
+ (0, import_react29.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react28.createElement)(CharacterGrid, {
+ (0, import_react29.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
@@ -12810,27 +12855,27 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
setLook((current) => ({ ...current, color }));
}
}),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
setImage(null);
setLook((current) => randomLook(current));
} }, t("Shuffle")),
- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
- image ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
- (0, import_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
+ image ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
+ (0, import_react29.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_react28.createElement)(
+ (0, import_react29.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react28.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react28.createElement)("input", {
+ (0, import_react29.createElement)("span", { className: "bt-field-label" }, t("Name")),
+ (0, import_react29.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
@@ -12845,59 +12890,59 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
}
}
}),
- problem ? (0, import_react28.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react28.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
+ problem ? (0, import_react29.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react29.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-field" },
- (0, import_react28.createElement)("span", { className: "bt-field-label" }, t("Model")),
- (0, import_react28.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
+ (0, import_react29.createElement)("span", { className: "bt-field-label" }, t("Model")),
+ (0, import_react29.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_react28.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_react29.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_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
- (0, import_react28.createElement)(
+ error ? (0, import_react29.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
+ (0, import_react29.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_react28.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
- (0, import_react28.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_react29.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
+ (0, import_react29.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_react28.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
- return (0, import_react28.createElement)(
+ if (inline) return (0, import_react29.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
+ return (0, import_react29.createElement)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-create-head" },
- canGoBack ? (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react28.createElement)(ChevronLeftIcon)) : (0, import_react28.createElement)("span", { className: "bt-create-gap" }),
- (0, import_react28.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
- (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react28.createElement)(CloseIcon))
+ canGoBack ? (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react29.createElement)(ChevronLeftIcon)) : (0, import_react29.createElement)("span", { className: "bt-create-gap" }),
+ (0, import_react29.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react29.createElement)(CloseIcon))
),
- (0, import_react28.createElement)("div", { className: "bt-create-body" }, card)
+ (0, import_react29.createElement)("div", { className: "bt-create-body" }, card)
);
}
// src/client/new-chat.js
-var import_react29 = require("react");
+var import_react30 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react29.useState)("");
- const [picked, setPicked] = (0, import_react29.useState)([]);
- const [group, setGroup] = (0, import_react29.useState)(mode === "group");
- const [compose, setCompose] = (0, import_react29.useState)(mode !== "new");
- const [admin, setAdmin] = (0, import_react29.useState)(null);
- const [message, setMessage] = (0, import_react29.useState)("");
- const [busy, setBusy] = (0, import_react29.useState)(false);
- const [error, setError] = (0, import_react29.useState)("");
- const [fix, setFix] = (0, import_react29.useState)(null);
- const [fixNote, setFixNote] = (0, import_react29.useState)("");
- const [inviteLink, setInviteLink] = (0, import_react29.useState)("");
- const [cursor, setCursor] = (0, import_react29.useState)(0);
- const [draft, setDraft] = (0, import_react29.useState)(mode === "create" ? { name: "", fromList: false } : null);
- const inputRef = (0, import_react29.useRef)(null);
+ const [query, setQuery] = (0, import_react30.useState)("");
+ const [picked, setPicked] = (0, import_react30.useState)([]);
+ const [group, setGroup] = (0, import_react30.useState)(mode === "group");
+ const [compose, setCompose] = (0, import_react30.useState)(mode !== "new");
+ const [admin, setAdmin] = (0, import_react30.useState)(null);
+ const [message, setMessage] = (0, import_react30.useState)("");
+ const [busy, setBusy] = (0, import_react30.useState)(false);
+ const [error, setError] = (0, import_react30.useState)("");
+ const [fix, setFix] = (0, import_react30.useState)(null);
+ const [fixNote, setFixNote] = (0, import_react30.useState)("");
+ const [inviteLink, setInviteLink] = (0, import_react30.useState)("");
+ const [cursor, setCursor] = (0, import_react30.useState)(0);
+ const [draft, setDraft] = (0, import_react30.useState)(mode === "create" ? { name: "", fromList: false } : null);
+ const inputRef = (0, import_react30.useRef)(null);
const close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -13028,14 +13073,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_react29.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
+ options.push({ key: "chat", icon: (0, import_react30.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_react29.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_react29.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
+ options.push({ key: "new", icon: (0, import_react30.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_react30.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
setGroup(true);
setCompose(true);
setCursor(0);
@@ -13044,7 +13089,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_react29.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react29.createElement)(PeopleIcon) }) : (0, import_react29.createElement)(PeopleIcon),
+ icon: action.icon ? (0, import_react30.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react30.createElement)(PeopleIcon) }) : (0, import_react30.createElement)(PeopleIcon),
label: t(action.label),
hint: action.hint ? t(action.hint) : void 0,
run: () => {
@@ -13056,7 +13101,7 @@ function NewChat({ mode, roster, actions }) {
});
}
if ((roster.agents ?? []).length === 0) {
- options.push({ key: "agent", icon: (0, import_react29.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_react30.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -13067,19 +13112,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_react29.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot });
+ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react30.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_react29.useRef)(null);
+ const drop = (0, import_react30.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react29.createElement)(CreateBot, {
+ (0, import_react30.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -13099,7 +13144,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_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{
className: "bt-pane",
@@ -13109,31 +13154,31 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
if (event.target === event.currentTarget) close();
} : void 0
},
- (0, import_react29.createElement)(
+ (0, import_react30.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_react29.createElement)(
+ menuOnly ? null : (0, import_react30.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react29.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react29.createElement)(
+ (0, import_react30.createElement)("span", { className: "bt-to-label" }, t("To:")),
+ adminBot ? (0, import_react30.createElement)(
"span",
{ key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") },
- (0, import_react29.createElement)(BotMark, { bot: adminBot, size: 16 }),
+ (0, import_react30.createElement)(BotMark, { bot: adminBot, size: 16 }),
adminBot.name,
- (0, import_react29.createElement)(AdminStar),
- (0, import_react29.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
+ (0, import_react30.createElement)(AdminStar),
+ (0, import_react30.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
) : null,
- choosingAdmin ? (0, import_react29.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react29.createElement)(AdminStar)) : null,
- pickedBots.map((bot) => (0, import_react29.createElement)(
+ choosingAdmin ? (0, import_react30.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react30.createElement)(AdminStar)) : null,
+ pickedBots.map((bot) => (0, import_react30.createElement)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react29.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react30.createElement)(BotMark, { bot, size: 16 }),
bot.name,
- (0, import_react29.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
+ (0, import_react30.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
)),
- (0, import_react29.createElement)("input", {
+ (0, import_react30.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
@@ -13157,20 +13202,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
} else if (event.key === "Backspace" && query === "") unpick();
}
}),
- group && admin !== null ? (0, import_react29.createElement)("button", {
+ group && admin !== null ? (0, import_react30.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_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
),
- (0, import_react29.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react29.createElement)(
+ (0, import_react30.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react30.createElement)(
"div",
{ ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" },
- (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.length === 0 ? (0, import_react29.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_react29.createElement)(
+ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ options.length === 0 ? (0, import_react30.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_react30.createElement)(
"button",
{
key: option.key,
@@ -13182,36 +13227,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
onMouseEnter: () => setCursor(index),
onClick: option.run
},
- option.bot ? (0, import_react29.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react29.createElement)("span", { className: "bt-option-icon" }, option.icon),
+ option.bot ? (0, import_react30.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react30.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_react29.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react29.createElement)("span", { className: "bt-option-copy" }, (0, import_react29.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react29.createElement)("span", { className: "bt-option-sub" }, option.hint)),
- option.bot && option.hint && index === active ? (0, import_react29.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
+ option.bot || !option.hint ? (0, import_react30.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react30.createElement)("span", { className: "bt-option-copy" }, (0, import_react30.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react30.createElement)("span", { className: "bt-option-sub" }, option.hint)),
+ option.bot && option.hint && index === active ? (0, import_react30.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
))
)),
- menuOnly ? null : (0, import_react29.createElement)("div", { className: "bt-nc-spacer" }),
- error && fix ? (0, import_react29.createElement)(
+ menuOnly ? null : (0, import_react30.createElement)("div", { className: "bt-nc-spacer" }),
+ error && fix ? (0, import_react30.createElement)(
"div",
{ className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
- (0, import_react29.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_react29.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_react29.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
+ (0, import_react30.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_react30.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_react30.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_react29.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_react30.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_react29.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
- inviteLink ? (0, import_react29.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_react29.createElement)("div", { className: "bt-error" }, error) : null,
- menuOnly ? null : (0, import_react29.createElement)(
+ fixNote ? (0, import_react30.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
+ inviteLink ? (0, import_react30.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_react30.createElement)("div", { className: "bt-error" }, error) : null,
+ menuOnly ? null : (0, import_react30.createElement)(
"div",
{ className: "bt-nc-compose" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-nc-card" },
- (0, import_react29.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react29.createElement)(PlusIcon)),
- (0, import_react29.createElement)("textarea", {
+ (0, import_react30.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react30.createElement)(PlusIcon)),
+ (0, import_react30.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
@@ -13224,7 +13269,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- (0, import_react29.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react29.createElement)(ArrowUpIcon))
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react30.createElement)(ArrowUpIcon))
)
)
)
@@ -13232,18 +13277,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
// src/client/exchange.js
-var import_react30 = require("react");
+var import_react31 = require("react");
var SHEET_EXIT_MS = 220;
function ExchangeDialog({ pair, roster, actions }) {
- const [entries2, setEntries] = (0, import_react30.useState)(null);
- const [leaving, setLeaving] = (0, import_react30.useState)(false);
+ const [entries2, setEntries] = (0, import_react31.useState)(null);
+ const [leaving, setLeaving] = (0, import_react31.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS);
};
useEscape(close);
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
@@ -13267,48 +13312,48 @@ function ExchangeDialog({ pair, roster, actions }) {
const right = roster.byId[pair[1]];
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react30.createElement)(
+ return (0, import_react31.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") },
- (0, import_react30.createElement)("div", { className: "bt-head" }, (0, import_react30.createElement)(
+ (0, import_react31.createElement)("div", { className: "bt-head" }, (0, import_react31.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react30.createElement)(BotMark, { bot: left, size: 22 }) : null,
+ left ? (0, import_react31.createElement)(BotMark, { bot: left, size: 22 }) : null,
left?.name ?? "?",
- (0, import_react30.createElement)("span", { className: "bt-swap" }, "⇄"),
- right ? (0, import_react30.createElement)(BotMark, { bot: right, size: 22 }) : null,
+ (0, import_react31.createElement)("span", { className: "bt-swap" }, "⇄"),
+ right ? (0, import_react31.createElement)(BotMark, { bot: right, size: 22 }) : null,
right?.name ?? "?"
)),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") },
- entries2 === null ? (0, import_react30.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react30.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
+ entries2 === null ? (0, import_react31.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react31.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_react30.createElement)(
- import_react30.Fragment,
+ return (0, import_react31.createElement)(
+ import_react31.Fragment,
{ key: entry.id },
- showDay ? (0, import_react30.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react30.createElement)(
+ showDay ? (0, import_react31.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
+ (0, import_react31.createElement)(
"div",
{ className: "bt-group-msg" },
- (0, import_react30.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
- (0, import_react30.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react30.createElement)("span", { className: "bt-lead" }, (0, import_react30.createElement)(BotMark, { bot, size: 22 })) })
+ (0, import_react31.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
+ (0, import_react31.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react31.createElement)("span", { className: "bt-lead" }, (0, import_react31.createElement)(BotMark, { bot, size: 22 })) })
)
);
})
),
- (0, import_react30.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react30.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
+ (0, import_react31.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
)
);
}
// src/client/voice.js
-var import_react31 = require("react");
+var import_react32 = 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",
@@ -13320,8 +13365,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_react31.useState)(0);
- (0, import_react31.useEffect)(() => {
+ const [level, setLevel] = (0, import_react32.useState)(0);
+ (0, import_react32.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -13371,18 +13416,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_react31.useState)(() => actions.speech());
- const [phase, setPhase] = (0, import_react31.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
- const [catalog, setCatalog] = (0, import_react31.useState)(null);
- const [heard, setHeard] = (0, import_react31.useState)("");
- const [said, setSaid] = (0, import_react31.useState)("");
- const [note, setNote] = (0, import_react31.useState)("");
- const [serviceLevel, setServiceLevel] = (0, import_react31.useState)(0);
- const [cycle, setCycle] = (0, import_react31.useState)(0);
- const waiting = (0, import_react31.useRef)(null);
- const reply = (0, import_react31.useRef)(lastReply);
+ const [speech] = (0, import_react32.useState)(() => actions.speech());
+ const [phase, setPhase] = (0, import_react32.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
+ const [catalog, setCatalog] = (0, import_react32.useState)(null);
+ const [heard, setHeard] = (0, import_react32.useState)("");
+ const [said, setSaid] = (0, import_react32.useState)("");
+ const [note, setNote] = (0, import_react32.useState)("");
+ const [serviceLevel, setServiceLevel] = (0, import_react32.useState)(0);
+ const [cycle, setCycle] = (0, import_react32.useState)(0);
+ const waiting = (0, import_react32.useRef)(null);
+ const reply = (0, import_react32.useRef)(lastReply);
reply.current = lastReply;
- const turn = (0, import_react31.useRef)(0);
+ const turn = (0, import_react32.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
@@ -13402,12 +13447,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
dictation.stop();
submit(text);
});
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
@@ -13422,16 +13467,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
};
}, [speech]);
const listen = phase === "listening";
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.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_react31.useEffect)(() => {
+ (0, import_react32.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
@@ -13493,7 +13538,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
setServiceLevel(0);
};
}, [speech, catalog, listen, cycle]);
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.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;
@@ -13513,7 +13558,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
}, [phase, running, lastReply]);
- (0, import_react31.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
+ (0, import_react32.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";
@@ -13530,44 +13575,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_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-voice-orb", style: { "--bt-level": level } },
- (0, import_react31.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
- (0, import_react31.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
- bot ? (0, import_react31.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
+ (0, import_react32.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
+ (0, import_react32.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
+ bot ? (0, import_react32.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react32.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
),
- (0, import_react31.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)("div", { className: "bt-voice-name" }, name),
+ (0, import_react32.createElement)(
"div",
{ className: "bt-voice-status", role: "status" },
- phase === "thinking" ? (0, import_react31.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react31.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
+ phase === "thinking" ? (0, import_react32.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react32.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
),
- (0, import_react31.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
- openSetup ? (0, import_react31.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
+ (0, import_react32.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
+ openSetup ? (0, import_react32.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
close();
openSetup();
} }, t("Set up voice input")) : null,
- shownNote ? (0, import_react31.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
+ shownNote ? (0, import_react32.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-voice-bar" },
- (0, import_react31.createElement)("button", {
+ (0, import_react32.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_react31.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react31.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
- (0, import_react31.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react31.createElement)(CloseIcon))
+ }, (0, import_react32.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react32.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
+ (0, import_react32.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react32.createElement)(CloseIcon))
)
);
}
@@ -13705,7 +13750,7 @@ function percent(part, whole) {
}
// src/client/usage-page.js
-var import_react32 = require("react");
+var import_react33 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -13752,9 +13797,9 @@ function titleOf(range, bucket) {
return dates({ year: "numeric", month: "long" }).format(bucket.start);
}
function useTween(value) {
- const [shown, setShown] = (0, import_react32.useState)(0);
- const from = (0, import_react32.useRef)(0);
- (0, import_react32.useEffect)(() => {
+ const [shown, setShown] = (0, import_react33.useState)(0);
+ const from = (0, import_react33.useRef)(0);
+ (0, import_react33.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
@@ -13774,9 +13819,9 @@ function useTween(value) {
}, [value]);
return shown;
}
-var glyphBox = (size, icon3) => (0, import_react32.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react32.createElement)(icon3));
+var glyphBox = (size, icon3) => (0, import_react33.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react33.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react32.createElement)("span", {
+ return (0, import_react33.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))` }
@@ -13793,7 +13838,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_react32.createElement)(BotAvatar, { bot, size, badge: false, live: false })
+ glyph: (size) => (0, import_react33.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
@@ -13803,7 +13848,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_react32.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
+ glyph: (size) => room2 ? (0, import_react33.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
@@ -13825,30 +13870,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_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label },
- (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.map(([id, text, icon3]) => (0, import_react32.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react32.createElement)(icon3) : null, text))
+ (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ options.map(([id, text, icon3]) => (0, import_react33.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react33.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_react32.createElement)("div", { className: "bt-us-stat" }, (0, import_react32.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react32.createElement)("span", { className: "bt-us-stat-value" }, value));
- return (0, import_react32.createElement)(
+ const cell = (label, value) => (0, import_react33.createElement)("div", { className: "bt-us-stat" }, (0, import_react33.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react33.createElement)("span", { className: "bt-us-stat-value" }, value));
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-stat bt-us-stat-main" },
- (0, import_react32.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
+ (0, import_react33.createElement)(
"span",
{ className: "bt-us-stat-value" },
compact(shown),
- (0, import_react32.createElement)("small", null, t("tokens")),
- change === null ? null : (0, import_react32.createElement)(
+ (0, import_react33.createElement)("small", null, t("tokens")),
+ change === null ? null : (0, import_react33.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
@@ -13866,83 +13911,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_react32.useState)(null);
+ const [hover, setHover] = (0, import_react33.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_react32.createElement)(
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-card-head" },
- (0, import_react32.createElement)("h3", null, t("Tokens over time"), (0, import_react32.createElement)("span", null, unitLabel(range.unit))),
- onBy ? (0, import_react32.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
+ (0, import_react33.createElement)("h3", null, t("Tokens over time"), (0, import_react33.createElement)("span", null, unitLabel(range.unit))),
+ onBy ? (0, import_react33.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) },
- [1, 0.5].map((level) => (0, import_react32.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react32.createElement)("span", null, compact(top * level)))),
- (0, import_react32.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react32.createElement)("span", null, "0")),
- (0, import_react32.createElement)(
+ [1, 0.5].map((level) => (0, import_react33.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react33.createElement)("span", null, compact(top * level)))),
+ (0, import_react33.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react33.createElement)("span", null, "0")),
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react32.createElement)("div", {
+ columns.map((column, at) => (0, import_react33.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_react32.createElement)(
+ }, column.total > 0 ? (0, import_react33.createElement)(
"div",
{ className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } },
- column.parts.map(([key, value]) => value > 0 ? (0, import_react32.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
- ) : (0, import_react32.createElement)("div", { className: "bt-us-stub" })))
+ column.parts.map(([key, value]) => value > 0 ? (0, import_react33.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
+ ) : (0, import_react33.createElement)("div", { className: "bt-us-stub" })))
),
- tip ? (0, import_react32.createElement)(
+ tip ? (0, import_react33.createElement)(
"div",
{ className: "bt-us-tip", style: tipSide },
- (0, import_react32.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
- (0, import_react32.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_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
+ (0, import_react33.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_react33.createElement)(
"div",
{ key, className: "bt-us-tip-row" },
- (0, import_react32.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
- (0, import_react32.createElement)("span", null, tipOf.get(key)?.name),
- (0, import_react32.createElement)("b", null, compact(value))
+ (0, import_react33.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
+ (0, import_react33.createElement)("span", null, tipOf.get(key)?.name),
+ (0, import_react33.createElement)("b", null, compact(value))
))
) : null
),
- (0, import_react32.createElement)(
+ (0, import_react33.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_react32.createElement)(
+ columns.map((column, at) => (0, import_react33.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_react32.createElement)(
+ series.length > 1 || series[0]?.pick ? (0, import_react33.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react32.createElement)(entry.pick ? "button" : "span", {
+ series.map((entry) => (0, import_react33.createElement)(entry.pick ? "button" : "span", {
key: entry.key,
className: "bt-us-key",
...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {}
- }, (0, import_react32.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
+ }, (0, import_react33.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_react32.createElement)(
+ return (0, import_react33.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
@@ -13951,69 +13996,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_react32.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react32.createElement)(
+ glyph ? (0, import_react33.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
+ (0, import_react33.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"span",
{ className: "bt-us-row-line" },
- (0, import_react32.createElement)("span", { className: "bt-us-row-name" }, name),
- sub ? (0, import_react32.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
+ (0, import_react33.createElement)("span", { className: "bt-us-row-name" }, name),
+ sub ? (0, import_react33.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
),
- (0, import_react32.createElement)("span", { className: "bt-us-track" }, (0, import_react32.createElement)("span", { style: { width: `${width}%`, background: fill2 } }))
+ (0, import_react33.createElement)("span", { className: "bt-us-track" }, (0, import_react33.createElement)("span", { style: { width: `${width}%`, background: fill2 } }))
),
- (0, import_react32.createElement)("span", { className: "bt-us-row-num" }, (0, import_react32.createElement)("b", null, compact(sum.total)), (0, import_react32.createElement)("small", null, `${percent(sum.total, whole)}%`))
+ (0, import_react33.createElement)("span", { className: "bt-us-row-num" }, (0, import_react33.createElement)("b", null, compact(sum.total)), (0, import_react33.createElement)("small", null, `${percent(sum.total, whole)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-card", style: { "--i": index } },
- (0, import_react32.createElement)("div", { className: "bt-us-card-head" }, (0, import_react32.createElement)("h3", null, title, count === void 0 ? null : (0, import_react32.createElement)("span", null, count))),
- (0, import_react32.createElement)("div", { className: "bt-us-rows" }, children)
+ (0, import_react33.createElement)("div", { className: "bt-us-card-head" }, (0, import_react33.createElement)("h3", null, title, count === void 0 ? null : (0, import_react33.createElement)("span", null, count))),
+ (0, import_react33.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_react32.createElement)(
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"svg",
{ viewBox: "0 0 96 96", "aria-hidden": true },
- face.stops ? (0, import_react32.createElement)("defs", null, (0, import_react32.createElement)(
+ face.stops ? (0, import_react33.createElement)("defs", null, (0, import_react33.createElement)(
"linearGradient",
{ id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 },
- face.stops.map(([offset, color]) => (0, import_react32.createElement)("stop", { key: offset, offset, stopColor: color }))
+ face.stops.map(([offset, color]) => (0, import_react33.createElement)("stop", { key: offset, offset, stopColor: color }))
)) : null,
- (0, import_react32.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
- arc > 0 ? (0, import_react32.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_react33.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
+ arc > 0 ? (0, import_react33.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_react32.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
+ (0, import_react33.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-profile-copy" },
- (0, import_react32.createElement)("h3", null, face.name),
- face.sub ? (0, import_react32.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
- (0, import_react32.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_react33.createElement)("h3", null, face.name),
+ face.sub ? (0, import_react33.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
+ (0, import_react33.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_react32.useRef)(null);
+ const box = (0, import_react33.useRef)(null);
const todayMs = hourMs(today);
const thisMonth = monthOf(todayMs);
- const [right, setRight] = (0, import_react32.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
- const [anchor, setAnchor] = (0, import_react32.useState)(null);
- const [hover, setHover] = (0, import_react32.useState)(null);
- (0, import_react32.useEffect)(() => {
+ const [right, setRight] = (0, import_react33.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
+ const [anchor, setAnchor] = (0, import_react33.useState)(null);
+ const [hover, setHover] = (0, import_react33.useState)(null);
+ (0, import_react33.useEffect)(() => {
const onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
@@ -14039,17 +14084,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
const [from, to] = [anchor, key].sort();
onPick(from, to);
};
- const weekdays = (0, import_react32.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []);
+ const weekdays = (0, import_react33.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_react32.createElement)("span", { key: `b${at}` }));
+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react33.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_react32.createElement)("button", {
+ cells.push((0, import_react33.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
@@ -14065,15 +14110,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
"data-has": flag(active.has(key)),
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react32.createElement)("b", null, day)));
+ }, (0, import_react33.createElement)("b", null, day)));
}
- const nav = side === "left" ? (0, import_react32.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_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.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_react32.createElement)(ChevronRightIcon));
- return (0, import_react32.createElement)(
+ const nav = side === "left" ? (0, import_react33.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_react33.createElement)(ChevronLeftIcon)) : (0, import_react33.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_react33.createElement)(ChevronRightIcon));
+ return (0, import_react33.createElement)(
"div",
{ key: start, className: "bt-us-month" },
- (0, import_react32.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react32.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
- (0, import_react32.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react32.createElement)("span", { key: at }, name))),
- (0, import_react32.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
+ (0, import_react33.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react33.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
+ (0, import_react33.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react33.createElement)("span", { key: at }, name))),
+ (0, import_react33.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
);
};
const shortcuts = [
@@ -14082,30 +14127,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_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-pick-quick" },
- shortcuts.map(([id, label, from, to]) => (0, import_react32.createElement)("button", {
+ shortcuts.map(([id, label, from, to]) => (0, import_react33.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
onClick: () => onPick(from, to)
}, label))
),
- (0, import_react32.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-pick-foot", role: "status" },
- (0, import_react32.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
- low === null ? null : (0, import_react32.createElement)("span", null, (0, import_react32.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
+ (0, import_react33.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
+ low === null ? null : (0, import_react33.createElement)("span", null, (0, import_react33.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
)
);
}
function Heat({ view, keep, paint, index }) {
- const [hover, setHover] = (0, import_react32.useState)(null);
+ const [hover, setHover] = (0, import_react33.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);
@@ -14121,13 +14166,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_react32.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
+ months.push((0, import_react33.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_react32.createElement)("span", {
+ cells.push((0, import_react33.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
@@ -14135,7 +14180,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_react32.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
+ }, level > 0 ? (0, import_react33.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
}
}
const tipSide = hover ? {
@@ -14144,61 +14189,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_react32.createElement)(
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-card bt-us-heat", style: { "--i": index } },
- (0, import_react32.createElement)("div", { className: "bt-us-card-head" }, (0, import_react32.createElement)("h3", null, t("Activity")), (0, import_react32.createElement)("span", { className: "bt-us-head-note" }, summary)),
- (0, import_react32.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-card-head" }, (0, import_react33.createElement)("h3", null, t("Activity")), (0, import_react33.createElement)("span", { className: "bt-us-head-note" }, summary)),
+ (0, import_react33.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) },
- (0, import_react32.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
- hover ? (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
+ hover ? (0, import_react33.createElement)(
"div",
{ className: "bt-us-tip bt-us-heat-tip", style: tipSide },
- (0, import_react32.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
- (0, import_react32.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
+ (0, import_react33.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
+ (0, import_react33.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
) : null
),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-heat-scale", "aria-hidden": true },
t("Light"),
- levels.map((level) => (0, import_react32.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react32.createElement)("i", { style: { background: soften(paint, level) } }))),
+ levels.map((level) => (0, import_react33.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react33.createElement)("i", { style: { background: soften(paint, level) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ className: "bt-us", "aria-busy": true },
- (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-sheet bt-us-skel-sheet" },
- (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
- (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
+ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-pair" },
- (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
- (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
+ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
+ (0, import_react33.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_react32.useState)(null);
- const [error, setError] = (0, import_react32.useState)("");
- const [rangeId, setRangeId] = (0, import_react32.useState)("7d");
- const [span, setSpan] = (0, import_react32.useState)(null);
- const [picking, setPicking] = (0, import_react32.useState)(false);
- const [by, setBy] = (0, import_react32.useState)("bot");
- const tabs = (0, import_react32.useRef)(null);
+ const [view, setView] = (0, import_react33.useState)(null);
+ const [error, setError] = (0, import_react33.useState)("");
+ const [rangeId, setRangeId] = (0, import_react33.useState)("7d");
+ const [span, setSpan] = (0, import_react33.useState)(null);
+ const [picking, setPicking] = (0, import_react33.useState)(false);
+ const [by, setBy] = (0, import_react33.useState)("bot");
+ const tabs = (0, import_react33.useRef)(null);
const focus = focusOf(focusId);
- (0, import_react32.useEffect)(() => {
+ (0, import_react33.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(() => {
@@ -14208,7 +14253,7 @@ function UsagePage({ roster, actions, focusId }) {
const value = await actions.usage();
await resized;
if (stopped) return;
- (0, import_react32.startTransition)(() => setView(value));
+ (0, import_react33.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
@@ -14225,7 +14270,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_react32.useMemo)(() => {
+ const shaped = (0, import_react33.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
@@ -14234,7 +14279,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_react32.createElement)(ModelGlyph, { name, paint, size }) };
+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react33.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;
@@ -14282,23 +14327,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_react32.useRef)(null);
- (0, import_react32.useEffect)(() => {
+ const root = (0, import_react33.useRef)(null);
+ (0, import_react33.useEffect)(() => {
root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 });
}, [focusId]);
if (!view) {
- return error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react32.createElement)(Skeleton2);
+ return error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react33.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-empty" },
- (0, import_react32.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
- (0, import_react32.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
- (0, import_react32.createElement)("span", null, t("Token usage shows up here after a model is called."))
+ (0, import_react33.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
+ (0, import_react33.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
+ (0, import_react33.createElement)("span", null, t("Token usage shows up here after a model is called."))
)
);
}
@@ -14323,7 +14368,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_react32.createElement)(Row2, {
+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react33.createElement)(Row2, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
@@ -14335,7 +14380,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_react32.createElement)(Row2, {
+ const modelRow = ([at, sum], index) => (0, import_react33.createElement)(Row2, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
@@ -14347,30 +14392,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_react32.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
+ const nothing = (0, import_react33.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_react32.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
+ (0, import_react33.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react32.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
- (0, import_react32.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
+ (0, import_react33.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
+ (0, import_react33.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- ownerAt >= 0 ? (0, import_react32.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react32.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_react32.createElement)(
+ ownerAt >= 0 ? (0, import_react33.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react33.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_react33.createElement)(
ListCard,
{ title: t("What for"), index: 3 },
- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react32.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_react33.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_react32.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
+ (0, import_react33.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
)
];
} else {
@@ -14379,39 +14424,39 @@ function UsagePage({ roster, actions, focusId }) {
const rest = owners.filter(([at]) => !isBot(at));
const usedModels = ranked(all.models);
body = [
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react32.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
- (0, import_react32.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
+ (0, import_react33.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
+ (0, import_react33.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react32.createElement)(
+ (0, import_react33.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_react32.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
+ bots.length > 0 && rest.length > 0 ? (0, import_react33.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
rest.map((entry, index) => ownerRow(entry, bots.length + index))
),
- (0, import_react32.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
+ (0, import_react33.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
),
- (0, import_react32.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
+ (0, import_react33.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
)
];
}
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-us-top" },
- (0, import_react32.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
- view.scanning ? (0, import_react32.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
- picking ? (0, import_react32.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
+ (0, import_react33.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
+ view.scanning ? (0, import_react33.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
+ picking ? (0, import_react33.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
),
- error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
+ error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
body
);
}
@@ -14568,7 +14613,7 @@ button.bt-us-row:hover{background:var(--bt-hover)}
`;
// src/client/connectors.js
-var import_react33 = require("react");
+var import_react34 = require("react");
var TOKEN_URL = "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/settings/personal-access-tokens/new";
var COMPOSIO_KEY_URL = "https://platform.composio.dev/";
var APPS = [
@@ -14601,10 +14646,10 @@ var APPS = [
["youtube", "YouTube"]
];
var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug;
-var GitHubMark = () => (0, import_react33.createElement)(
+var GitHubMark = () => (0, import_react34.createElement)(
"svg",
{ width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true },
- (0, import_react33.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_react34.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 = {
@@ -14621,20 +14666,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_react33.createElement)(
+ return (0, import_react34.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_react33.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ label ? (0, import_react34.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
);
}
function AppPicker({ apps, onChange, search, statuses = {} }) {
- const [query, setQuery] = (0, import_react33.useState)("");
- const [found, setFound] = (0, import_react33.useState)(null);
- const [failed, setFailed] = (0, import_react33.useState)(null);
- const names = (0, import_react33.useRef)(new Map(APPS));
- const asked = (0, import_react33.useRef)(0);
- (0, import_react33.useEffect)(() => {
+ const [query, setQuery] = (0, import_react34.useState)("");
+ const [found, setFound] = (0, import_react34.useState)(null);
+ const [failed, setFailed] = (0, import_react34.useState)(null);
+ const names = (0, import_react34.useRef)(new Map(APPS));
+ const asked = (0, import_react34.useRef)(0);
+ (0, import_react34.useEffect)(() => {
const text = query.trim();
if (text === "" || search === void 0) {
setFound(null);
@@ -14661,13 +14706,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_react33.createElement)(
+ return (0, import_react34.createElement)(
"div",
{ className: "bt-connector-apps" },
- apps.length > 0 ? (0, import_react33.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react33.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
- (0, import_react33.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_react33.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
- (0, import_react33.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react33.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react33.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
+ apps.length > 0 ? (0, import_react34.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react34.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
+ (0, import_react34.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_react34.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
+ (0, import_react34.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react34.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)))
@@ -14675,21 +14720,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_react33.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react33.createElement)(
+ return (0, import_react34.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react34.createElement)(
"li",
{ key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
- (0, import_react33.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
- (0, import_react33.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
+ (0, import_react34.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
+ (0, import_react34.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
)));
}
function ConnectorRow({ connector, roster, actions }) {
- const [form, setForm] = (0, import_react33.useState)(false);
- const [bots, setBots] = (0, import_react33.useState)(false);
- const [token, setToken] = (0, import_react33.useState)("");
- const [apps, setApps] = (0, import_react33.useState)(() => connector.apps ?? []);
- const [picking, setPicking] = (0, import_react33.useState)(false);
- const [busy, setBusy] = (0, import_react33.useState)(false);
- const [error, setError] = (0, import_react33.useState)(null);
+ const [form, setForm] = (0, import_react34.useState)(false);
+ const [bots, setBots] = (0, import_react34.useState)(false);
+ const [token, setToken] = (0, import_react34.useState)("");
+ const [apps, setApps] = (0, import_react34.useState)(() => connector.apps ?? []);
+ const [picking, setPicking] = (0, import_react34.useState)(false);
+ const [busy, setBusy] = (0, import_react34.useState)(false);
+ const [error, setError] = (0, import_react34.useState)(null);
const run = (task, after) => {
setBusy(true);
setError(null);
@@ -14698,9 +14743,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_react33.useState)({});
+ const [statuses, setStatuses] = (0, import_react34.useState)({});
const appsKey = (connector.apps ?? []).join(",");
- (0, import_react33.useEffect)(() => {
+ (0, import_react34.useEffect)(() => {
if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
setStatuses({});
return void 0;
@@ -14715,7 +14760,7 @@ function ConnectorRow({ connector, roster, actions }) {
live = false;
};
}, [gateway, connector.state, appsKey, picking]);
- const search = (0, import_react33.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
+ const search = (0, import_react34.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) {
@@ -14737,88 +14782,88 @@ function ConnectorRow({ connector, roster, actions }) {
};
const on = connector.state !== "off";
const Mark = MARKS[connector.id];
- const row = (0, import_react33.useRef)(null);
+ const row = (0, import_react34.useRef)(null);
useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`);
- return (0, import_react33.createElement)(
+ return (0, import_react34.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react33.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react33.createElement)(Mark) : null),
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react34.createElement)(Mark) : null),
+ (0, import_react34.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react33.createElement)("span", { className: "bt-connector-name" }, connector.name),
- (0, import_react33.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
+ (0, import_react34.createElement)("span", { className: "bt-connector-name" }, connector.name),
+ (0, import_react34.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
),
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"span",
{ className: "bt-connector-actions" },
- connector.state === "error" ? (0, import_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ connector.state === "error" ? (0, import_react34.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_react34.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_react33.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_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
+ on ? (0, import_react34.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_react34.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_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
)
),
- on && gateway && !picking ? (0, import_react33.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
- on ? (0, import_react33.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
- on && bots ? (0, import_react33.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
- !on && form ? (0, import_react33.createElement)(
+ on && gateway && !picking ? (0, import_react34.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
+ on ? (0, import_react34.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
+ on && bots ? (0, import_react34.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
+ !on && form ? (0, import_react34.createElement)(
"div",
{ className: "bt-connector-form" },
- (0, import_react33.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react33.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
- gateway ? (0, import_react33.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react34.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
+ gateway ? (0, import_react34.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
+ (0, import_react34.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ (0, import_react34.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_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setForm(false);
setToken("");
setError(null);
} }, t("Cancel")),
- (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect"))
+ (0, import_react34.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_react33.createElement)(
+ on && gateway && picking ? (0, import_react34.createElement)(
"div",
{ className: "bt-connector-form" },
- (0, import_react33.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
- (0, import_react33.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_react33.createElement)(
+ (0, import_react34.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
+ (0, import_react34.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_react34.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react33.createElement)("span", { className: "bt-connector-link" }),
- (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ (0, import_react34.createElement)("span", { className: "bt-connector-link" }),
+ (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setPicking(false);
setError(null);
} }, t("Cancel")),
- (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save"))
+ (0, import_react34.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_react33.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
+ error ? (0, import_react34.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
);
}
function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? [];
- return (0, import_react33.createElement)(
+ return (0, import_react34.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react33.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-set-row" }, (0, import_react33.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react33.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
+ (0, import_react34.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-set-row" }, (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react34.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
);
}
// src/client/feedback.js
-var import_react34 = require("react");
-var FeedbackIcon = () => (0, import_react34.createElement)(
+var import_react35 = require("react");
+var FeedbackIcon = () => (0, import_react35.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_react34.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_react34.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" })
+ (0, import_react35.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_react35.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" }));
@@ -14841,16 +14886,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_react34.useState)("");
- const [report, setReport] = (0, import_react34.useState)(null);
- const [preview, setPreview] = (0, import_react34.useState)(false);
- const [note, setNote] = (0, import_react34.useState)("");
+ const [what, setWhat] = (0, import_react35.useState)("");
+ const [report, setReport] = (0, import_react35.useState)(null);
+ const [preview, setPreview] = (0, import_react35.useState)(false);
+ const [note, setNote] = (0, import_react35.useState)("");
const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => {
setReport(next);
return next;
});
const load = () => fetchReport().then(() => void 0);
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
@@ -14858,7 +14903,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_react34.useState)(null);
+ const [issueLink, setIssueLink] = (0, import_react35.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
@@ -14886,59 +14931,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
const total = (report?.total ?? 0) + browserErrors.length;
const shown = entries2.slice(0, 8);
return [
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
"label",
{ className: "bt-set-copy", htmlFor: "bt-fb-what" },
- (0, import_react34.createElement)("span", null, t("What happened?")),
- (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
+ (0, import_react35.createElement)("span", null, t("What happened?")),
+ (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
),
- (0, import_react34.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_react35.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_react34.createElement)(
+ (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
- (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
),
- preview ? (0, import_react34.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react34.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
- (0, import_react34.createElement)(
+ preview ? (0, import_react35.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react35.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
+ (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react34.createElement)(
+ note ? (0, import_react35.createElement)(
"div",
{ key: "note", className: "bt-note bt-fb-note", role: "status" },
note,
- issueLink ? [" ", (0, import_react34.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
+ issueLink ? [" ", (0, import_react35.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
) : null,
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
Section,
{ key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") },
- shown.length === 0 ? (0, import_react34.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_react34.createElement)(
+ shown.length === 0 ? (0, import_react35.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_react35.createElement)(
"div",
{ key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text },
- (0, import_react34.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
+ (0, import_react35.createElement)(
"div",
{ className: "bt-set-copy" },
- (0, import_react34.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
- (0, import_react34.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · "))
+ (0, import_react35.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
+ (0, import_react35.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_react34.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
+ entry.code ? (0, import_react35.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
)),
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
Row3,
{ label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") },
- (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
- (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
)
)
];
@@ -14946,8 +14991,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_react34.useState)(false);
- (0, import_react34.useEffect)(() => {
+ const [leaving, setLeaving] = (0, import_react35.useState)(false);
+ (0, import_react35.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
@@ -14959,15 +15004,15 @@ function ErrorToast({ useUi, actions }) {
}, [toast?.token]);
if (!toast) return null;
const info = toast.tone === "info";
- return (0, import_react34.createElement)(
+ return (0, import_react35.createElement)(
"div",
{ className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 },
- (0, import_react34.createElement)("span", { className: "bt-toast-text" }, toast.text),
- info ? null : (0, import_react34.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
+ (0, import_react35.createElement)("span", { className: "bt-toast-text" }, toast.text),
+ info ? null : (0, import_react35.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
actions.dismissToast(toast.token);
actions.openSettings("feedback");
} }, t("Details")),
- (0, import_react34.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react34.createElement)(CloseIcon))
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react35.createElement)(CloseIcon))
);
}
var FEEDBACK_CSS = `
@@ -14998,7 +15043,7 @@ var FEEDBACK_CSS = `
`;
// src/client/about.js
-var import_react35 = require("react");
+var import_react36 = 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…");
@@ -15033,9 +15078,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_react35.useState)(false);
- const [stars, setStars] = (0, import_react35.useState)(null);
- (0, import_react35.useEffect)(() => {
+ const [checking, setChecking] = (0, import_react36.useState)(false);
+ const [stars, setStars] = (0, import_react36.useState)(null);
+ (0, import_react36.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
@@ -15056,29 +15101,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_react35.createElement)(
+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react36.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_react35.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_react35.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_react35.createElement)(
+ ) : (0, import_react36.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_react36.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_react36.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_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ key: "hero", className: "bt-about-hero" },
- (0, import_react35.createElement)("span", { className: "bt-about-mark" }, (0, import_react35.createElement)(FlyBotMark, { size: 52 })),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("span", { className: "bt-about-mark" }, (0, import_react36.createElement)(FlyBotMark, { size: 52 })),
+ (0, import_react36.createElement)(
"div",
{ className: "bt-about-copy" },
- (0, import_react35.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
- update.current ? (0, import_react35.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
+ (0, import_react36.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
+ update.current ? (0, import_react36.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
),
- repo ? (0, import_react35.createElement)(
+ repo ? (0, import_react36.createElement)(
"a",
{
className: "bt-star",
@@ -15088,23 +15133,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_react35.createElement)("span", { className: "bt-star-main" }, (0, import_react35.createElement)("span", { className: "bt-star-icon" }, (0, import_react35.createElement)(StarLineIcon)), "Star"),
- stars === null ? null : (0, import_react35.createElement)("span", { className: "bt-star-count" }, starCount(stars))
+ (0, import_react36.createElement)("span", { className: "bt-star-main" }, (0, import_react36.createElement)("span", { className: "bt-star-icon" }, (0, import_react36.createElement)(StarLineIcon)), "Star"),
+ stars === null ? null : (0, import_react36.createElement)("span", { className: "bt-star-count" }, starCount(stars))
) : null
),
- repo ? (0, import_react35.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_react35.createElement)(
+ repo ? (0, import_react36.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_react36.createElement)(
Section,
{ key: "updates", title: t("Updates") },
- (0, import_react35.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react35.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
- dsh?.current ? (0, import_react35.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
+ (0, import_react36.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react36.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
+ dsh?.current ? (0, import_react36.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
Section,
{ key: "links", title: t("Links") },
- repo ? (0, import_react35.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react35.createElement)("a", linkProps(repo), (0, import_react35.createElement)(GitHubMark), t("Open"))) : null,
- update.notesUrl ? (0, import_react35.createElement)(Row3, { label: t("Release notes") }, (0, import_react35.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
- (0, import_react35.createElement)(Row3, { label: t("Report a problem") }, (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
+ repo ? (0, import_react36.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react36.createElement)("a", linkProps(repo), (0, import_react36.createElement)(GitHubMark), t("Open"))) : null,
+ update.notesUrl ? (0, import_react36.createElement)(Row3, { label: t("Release notes") }, (0, import_react36.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
+ (0, import_react36.createElement)(Row3, { label: t("Report a problem") }, (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
)
];
}
@@ -15133,19 +15178,19 @@ var ABOUT_CSS = `
`;
// src/client/settings.js
-var import_react36 = require("react");
+var import_react37 = 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_react36.createElement)("section", { className: "bt-set-section" }, (0, import_react36.createElement)("h3", null, title), (0, import_react36.createElement)("div", { className: "bt-set-card" }, children));
+ return (0, import_react37.createElement)("section", { className: "bt-set-section" }, (0, import_react37.createElement)("h3", null, title), (0, import_react37.createElement)("div", { className: "bt-set-card" }, children));
}
function SetRow({ label, hint, children }) {
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react36.createElement)("div", { className: "bt-set-copy" }, (0, import_react36.createElement)("span", null, label), hint ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- children ? (0, import_react36.createElement)("div", { className: "bt-set-control" }, children) : null
+ (0, import_react37.createElement)("div", { className: "bt-set-copy" }, (0, import_react37.createElement)("span", null, label), hint ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ children ? (0, import_react37.createElement)("div", { className: "bt-set-control" }, children) : null
);
}
var NOTIFY_KINDS = [
@@ -15157,35 +15202,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_react36.useState)(prefs.accent ?? "#4d6bfe");
- const timer = (0, import_react36.useRef)(0);
- (0, import_react36.useEffect)(() => () => clearTimeout(timer.current), []);
+ const [custom, setCustom] = (0, import_react37.useState)(prefs.accent ?? "#4d6bfe");
+ const timer = (0, import_react37.useRef)(0);
+ (0, import_react37.useEffect)(() => () => clearTimeout(timer.current), []);
const pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
};
return [
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)("option", { key: id, value: id }, t(theme.label)))
+ Object.entries(themes).map(([id, theme]) => (0, import_react37.createElement)("option", { key: id, value: id }, t(theme.label)))
)
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-swatches" },
- (0, import_react36.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_react36.createElement)("button", {
+ (0, import_react37.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_react37.createElement)("button", {
key: ink,
type: "button",
className: "bt-swatch",
@@ -15195,26 +15240,26 @@ function GeneralPage({ roster, actions, run }) {
style: { background: INKS[ink][0] },
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
+ (0, import_react37.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_react36.createElement)(
+ (0, import_react37.createElement)(
SetSection,
{ key: "notify", title: t("Notifications") },
- (0, import_react36.createElement)(SwitchRow, {
+ (0, import_react37.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_react36.createElement)(SwitchRow, {
+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react37.createElement)(SwitchRow, {
key: kind,
label: t(label),
hint: hint ? t(hint) : void 0,
@@ -15230,52 +15275,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_react36.createElement)(
+ return (0, import_react37.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)("select", {
+ (0, import_react37.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_react36.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react36.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
+ }, first ? null : (0, import_react37.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react37.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
),
- others.map((bot) => (0, import_react36.createElement)(
+ others.map((bot) => (0, import_react37.createElement)(
SetRow,
{ key: bot.id, label: bot.name, hint: t("Also a Main Bot.") },
- (0, import_react36.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_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
+ (0, import_react37.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_react37.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_react36.createElement)(
+ candidates.length > 0 ? (0, import_react37.createElement)(
SetRow,
{ label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") },
- (0, import_react36.createElement)("select", {
+ (0, import_react37.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_react36.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react36.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
+ }, (0, import_react37.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react37.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_react36.createElement)(
+ return (0, import_react37.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
SetRow,
{ label: t("Bot animation") },
- (0, import_react36.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react36.createElement)("button", {
+ (0, import_react37.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react37.createElement)("button", {
key: id,
type: "button",
"aria-pressed": motion === id,
@@ -15287,69 +15332,69 @@ function MotionSection({ roster, actions, run }) {
);
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react36.useState)(false);
+ const [creating, setCreating] = (0, import_react37.useState)(false);
return [
- (0, import_react36.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
- (0, import_react36.createElement)(MotionSection, { key: "motion", roster, actions, run }),
- creating ? (0, import_react36.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react36.createElement)(
+ (0, import_react37.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
+ (0, import_react37.createElement)(MotionSection, { key: "motion", roster, actions, run }),
+ creating ? (0, import_react37.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react37.createElement)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react36.createElement)(PlusIcon), t("New Bot"))
+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react37.createElement)(PlusIcon), t("New Bot"))
),
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)(
+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react37.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react36.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
- (0, import_react36.createElement)("span", { className: "bt-row-title" }, bot.name),
- bot.role ? (0, import_react36.createElement)("span", { className: "bt-tag" }, bot.role) : null,
- isMainOf(roster, bot.id) ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
+ (0, import_react37.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
+ (0, import_react37.createElement)("span", { className: "bt-row-title" }, bot.name),
+ bot.role ? (0, import_react37.createElement)("span", { className: "bt-tag" }, bot.role) : null,
+ isMainOf(roster, bot.id) ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
+ (0, import_react37.createElement)("span", { className: "bt-chevron" }, (0, import_react37.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react36.useState)(false);
+ const [creating, setCreating] = (0, import_react37.useState)(false);
return [
- creating ? (0, import_react36.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react36.createElement)(
+ creating ? (0, import_react37.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react37.createElement)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react36.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_react36.createElement)(PlusIcon), t("Create group chat"))
+ (0, import_react37.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_react37.createElement)(PlusIcon), t("Create group chat"))
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
SetSection,
{ key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) },
- roster.rooms.length === 0 ? (0, import_react36.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_react37.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_react36.createElement)(
+ return (0, import_react37.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react36.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
- (0, import_react36.createElement)("span", { className: "bt-row-title" }, room2.name),
- (0, import_react36.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
- admin ? (0, import_react36.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react36.createElement)(AdminStar), admin.name) : null,
- room2.hidden ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
+ (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
+ (0, import_react37.createElement)("span", { className: "bt-row-title" }, room2.name),
+ (0, import_react37.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
+ admin ? (0, import_react37.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react37.createElement)(AdminStar), admin.name) : null,
+ room2.hidden ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
+ (0, import_react37.createElement)("span", { className: "bt-chevron" }, (0, import_react37.createElement)(ChevronRightIcon))
);
})
)
];
}
function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
- const [error, setError] = (0, import_react36.useState)("");
- const close = (0, import_react36.useCallback)(() => actions.closeSettings(), []);
+ const [error, setError] = (0, import_react37.useState)("");
+ const close = (0, import_react37.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
@@ -15370,34 +15415,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_react36.useRef)({ key: pageKey, depth, move: "up" });
+ const last = (0, import_react37.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_react36.useState)(shown === "usage");
+ const [usageOpened, setUsageOpened] = (0, import_react37.useState)(shown === "usage");
if (shown === "usage" && !usageOpened) setUsageOpened(true);
- const errorLine = error ? (0, import_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
- return (0, import_react36.createElement)(
+ const errorLine = error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ return (0, import_react37.createElement)(
"div",
{ className: "bt-settings-layer", "data-leaving": leaving || void 0 },
- (0, import_react36.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"nav",
{ ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") },
- (0, import_react36.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
- pages.map(([key, text]) => (0, import_react36.createElement)("button", {
+ (0, import_react37.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
+ pages.map(([key, text]) => (0, import_react37.createElement)("button", {
key,
type: "button",
className: "bt-settings-item",
"aria-current": shown === key ? "page" : void 0,
onClick: () => actions.openSettings(key)
- }, (0, import_react36.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))),
+ }, (0, import_react37.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_react36.createElement)("button", {
+ (0, import_react37.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"),
@@ -15406,51 +15451,51 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
close();
openHarnessSettings();
}
- }, (0, import_react36.createElement)(SlidersIcon), t("Harness settings"), (0, import_react36.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
+ }, (0, import_react37.createElement)(SlidersIcon), t("Harness settings"), (0, import_react37.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-settings-head" },
- back ? (0, import_react36.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react36.createElement)(ChevronLeftIcon)) : null,
- (0, import_react36.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
+ back ? (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react37.createElement)(ChevronLeftIcon)) : null,
+ (0, import_react37.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
),
- shown === "usage" ? null : (0, import_react36.createElement)(
+ shown === "usage" ? null : (0, import_react37.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
- shown === "general" ? (0, import_react36.createElement)(GeneralPage, { roster, actions, run }) : null,
- bot ? (0, import_react36.createElement)("div", { className: "bt-settings-form" }, (0, import_react36.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
- shown === "bots" && !bot ? (0, import_react36.createElement)(BotsPage, { roster, actions, run }) : null,
- room2 ? (0, import_react36.createElement)("div", { className: "bt-settings-form" }, (0, import_react36.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
- shown === "groups" && !room2 ? (0, import_react36.createElement)(GroupsPage, { roster, actions, run }) : null,
- shown === "connectors" ? (0, import_react36.createElement)(ConnectorsPage, { roster, actions }) : null,
- pluginPage ? (0, import_react36.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
- shown === "capabilities" ? (0, import_react36.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
- shown === "schedules" ? (0, import_react36.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
- shown === "secrets" ? (0, import_react36.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
- shown === "feedback" ? (0, import_react36.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
- shown === "about" ? (0, import_react36.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
+ shown === "general" ? (0, import_react37.createElement)(GeneralPage, { roster, actions, run }) : null,
+ bot ? (0, import_react37.createElement)("div", { className: "bt-settings-form" }, (0, import_react37.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
+ shown === "bots" && !bot ? (0, import_react37.createElement)(BotsPage, { roster, actions, run }) : null,
+ room2 ? (0, import_react37.createElement)("div", { className: "bt-settings-form" }, (0, import_react37.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
+ shown === "groups" && !room2 ? (0, import_react37.createElement)(GroupsPage, { roster, actions, run }) : null,
+ shown === "connectors" ? (0, import_react37.createElement)(ConnectorsPage, { roster, actions }) : null,
+ pluginPage ? (0, import_react37.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
+ shown === "capabilities" ? (0, import_react37.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
+ shown === "schedules" ? (0, import_react37.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
+ shown === "secrets" ? (0, import_react37.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
+ shown === "feedback" ? (0, import_react37.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
+ shown === "about" ? (0, import_react37.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
),
- usageOpened ? (0, import_react36.createElement)(
+ usageOpened ? (0, import_react37.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_react36.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
+ (0, import_react37.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
) : null
),
- (0, import_react36.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react36.createElement)(CloseIcon))
+ (0, import_react37.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react37.createElement)(CloseIcon))
)
);
}
// src/client/context-menu.js
-var import_react37 = require("react");
+var import_react38 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react37.useEffect)(() => {
+ (0, import_react38.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -15458,14 +15503,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
return () => window.removeEventListener("mousedown", onDown);
}, []);
const structure = useSidebarStructure();
- const ref = (0, import_react37.useRef)(null);
- const [place, setPlace] = (0, import_react37.useState)({ left: menu.x, top: menu.y });
- (0, import_react37.useLayoutEffect)(() => {
+ const ref = (0, import_react38.useRef)(null);
+ const [place, setPlace] = (0, import_react38.useState)({ left: menu.x, top: menu.y });
+ (0, import_react38.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_react37.createElement)(AgentMenu, { menu, roster, actions, leaving });
+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react38.createElement)(AgentMenu, { menu, roster, actions, leaving });
const bot = roster.byId[menu.id];
const room2 = roster.roomsById[menu.id];
const entry = bot ?? room2;
@@ -15507,14 +15552,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
]
];
const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
- return (0, import_react37.createElement)(
+ return (0, import_react38.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_react37.createElement)(
- import_react37.Fragment,
+ shown.map((group, index) => (0, import_react38.createElement)(
+ import_react38.Fragment,
{ key: index },
- index > 0 ? (0, import_react37.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react37.createElement)("button", {
+ index > 0 ? (0, import_react38.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, danger]) => (0, import_react38.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -15523,14 +15568,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react37.createElement)(Icon), label))
+ }, (0, import_react38.createElement)(Icon), label))
))
);
}
function AgentMenu({ menu, roster, actions, leaving }) {
- const ref = (0, import_react37.useRef)(null);
- const [place, setPlace] = (0, import_react37.useState)({ left: menu.x, top: menu.y });
- (0, import_react37.useLayoutEffect)(() => {
+ const ref = (0, import_react38.useRef)(null);
+ const [place, setPlace] = (0, import_react38.useState)({ left: menu.x, top: menu.y });
+ (0, import_react38.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)) });
@@ -15552,14 +15597,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
[t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
]
];
- return (0, import_react37.createElement)(
+ return (0, import_react38.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_react37.createElement)(
- import_react37.Fragment,
+ groups.map((group, index) => (0, import_react38.createElement)(
+ import_react38.Fragment,
{ key: index },
- index > 0 ? (0, import_react37.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react37.createElement)("button", {
+ index > 0 ? (0, import_react38.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, danger]) => (0, import_react38.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -15568,7 +15613,7 @@ function AgentMenu({ menu, roster, actions, leaving }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react37.createElement)(Icon), label))
+ }, (0, import_react38.createElement)(Icon), label))
))
);
}
@@ -15654,7 +15699,7 @@ function migrateStorage(storage = globalThis.localStorage) {
}
// src/client/details-drawer.js
-var import_react38 = require("react");
+var import_react39 = 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 "";
@@ -15679,10 +15724,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_react38.useState)(null);
+ const [pending, setPending] = (0, import_react39.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
@@ -15692,14 +15737,14 @@ function PermissionRow({ bot, roster, actions, run }) {
setPending(value);
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
+ (0, import_react39.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react38.createElement)("button", {
+ catalog.options.map((option) => (0, import_react39.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
@@ -15713,42 +15758,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_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
actions.browserEnsure?.(bot.id);
}, [bot.id]);
if (roster.browser !== true || !actions.browserShow) {
- return (0, import_react38.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
+ return (0, import_react39.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
}
if (state.ids.length === 0) {
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ className: "bt-browser-empty-mark" },
- (0, import_react38.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
- (0, import_react38.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
- (0, import_react38.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_react38.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
+ (0, import_react39.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
+ (0, import_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
+ (0, import_react39.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_react39.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
);
}
- return (0, import_react38.createElement)(
+ return (0, import_react39.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_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } },
- (0, import_react38.createElement)(
+ (0, import_react39.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_react38.createElement)(
+ page.excerpt ? (0, import_react39.createElement)(
"span",
{ className: "bt-bcard-preview" },
- (0, import_react38.createElement)("span", { className: "bt-bcard-ptitle" }, label),
- (0, import_react38.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
- ) : (0, import_react38.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react38.createElement)(GlobeIcon), (0, import_react38.createElement)("span", null, label))
+ (0, import_react39.createElement)("span", { className: "bt-bcard-ptitle" }, label),
+ (0, import_react39.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
+ ) : (0, import_react39.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react39.createElement)(GlobeIcon), (0, import_react39.createElement)("span", null, label))
),
- (0, import_react38.createElement)("button", {
+ (0, import_react39.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
@@ -15757,36 +15802,36 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
void actions.browserCloseTab(bot.id, tabId);
}
}, "×"),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-bcard-row" },
- (0, import_react38.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
- (0, import_react38.createElement)("span", { className: "bt-bcard-title" }, label),
- page.url ? (0, import_react38.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
+ (0, import_react39.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
+ (0, import_react39.createElement)("span", { className: "bt-bcard-title" }, label),
+ page.url ? (0, import_react39.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
)
);
}),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react38.createElement)(PlusIcon), t("New tab"))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react39.createElement)(PlusIcon), t("New tab"))
);
}
function About({ text, title = "About" }) {
- const [open, setOpen] = (0, import_react38.useState)(false);
+ const [open, setOpen] = (0, import_react39.useState)(false);
const long = text.split("\n").length > 5 || text.length > 240;
- const body = (0, import_react38.useRef)(null);
+ const body = (0, import_react39.useRef)(null);
useMorph(body, open);
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
null,
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t(title)),
- (0, import_react38.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
- long ? (0, import_react38.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t(title)),
+ (0, import_react39.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
+ long ? (0, import_react39.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_react38.useState)(room2.pendingApprovals ?? []);
- const [busy, setBusy] = (0, import_react38.useState)("");
+ const [items, setItems] = (0, import_react39.useState)(room2.pendingApprovals ?? []);
+ const [busy, setBusy] = (0, import_react39.useState)("");
const face = room2.origin === "face-to-face";
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
if (haveItems) setItems(room2.pendingApprovals);
if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -15812,28 +15857,28 @@ function SharedRoomApprovals({ room: room2 }) {
setBusy("");
}
};
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react38.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
- items.map((item) => (0, import_react38.createElement)(
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react39.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
+ items.map((item) => (0, import_react39.createElement)(
"div",
{ key: item.id, className: "bt-set-row" },
- (0, import_react38.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
+ (0, import_react39.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
+ (0, import_react39.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_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
null,
- (0, import_react38.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
- (0, import_react38.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react38.createElement)(FacePersonRow, {
+ (0, import_react39.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
+ (0, import_react39.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react39.createElement)(FacePersonRow, {
key: person.key,
person,
roster,
@@ -15857,79 +15902,79 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
};
const face = room2.origin === "face-to-face";
return [
- (0, import_react38.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
- invited && room2.sharedFrom && !face ? (0, import_react38.createElement)(
+ (0, import_react39.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
+ invited && room2.sharedFrom && !face ? (0, import_react39.createElement)(
"div",
{ key: "from", className: "bt-shared-from" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, title),
- (0, import_react38.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_react38.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
- (0, import_react38.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
- ) : face ? null : room2.notice ? (0, import_react38.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react38.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
- face ? null : (0, import_react38.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
- face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react38.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react38.createElement)(
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, title),
+ (0, import_react39.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_react39.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
+ ) : face ? null : room2.notice ? (0, import_react39.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react39.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
+ face ? null : (0, import_react39.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react39.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react39.createElement)(
"div",
{ key: "members" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
- (0, import_react38.createElement)("div", { className: "bt-members", role: "list" }, [
- ...humans.map((person) => (0, import_react38.createElement)(
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
+ (0, import_react39.createElement)("div", { className: "bt-members", role: "list" }, [
+ ...humans.map((person) => (0, import_react39.createElement)(
"div",
{ key: person.id, role: "listitem" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react38.createElement)("span", { className: "bt-member-name" }, (0, import_react38.createElement)("span", null, person.displayName || person.id)),
- (0, import_react38.createElement)("span", { className: "bt-chip" }, t("Human")),
- person.role === "admin" ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ (0, import_react39.createElement)("span", { className: "bt-member-name" }, (0, import_react39.createElement)("span", null, person.displayName || person.id)),
+ (0, import_react39.createElement)("span", { className: "bt-chip" }, t("Human")),
+ person.role === "admin" ? (0, import_react39.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
)
)),
- ...remotes.map((person) => (0, import_react38.createElement)(
+ ...remotes.map((person) => (0, import_react39.createElement)(
"div",
{ key: person.handle || person.id, role: "listitem" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"span",
{ className: "bt-member-name" },
- (0, import_react38.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react38.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
+ (0, import_react39.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
+ (0, import_react39.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
),
- face ? null : (0, import_react38.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ face ? null : (0, import_react39.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
)
)),
...members.map((member) => {
const entry = entries2.find((item) => item.id === member.id);
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ key: member.id, role: "listitem" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"button",
{ type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
- (0, import_react38.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
- (0, import_react38.createElement)("span", null, member.name)
+ (0, import_react39.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
+ (0, import_react39.createElement)("span", null, member.name)
),
- room2.admin === member.id ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ room2.admin === member.id ? (0, import_react39.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
),
- entry ? (0, import_react38.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ entry ? (0, import_react39.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
);
})
])
),
- face ? null : (0, import_react38.createElement)(
+ face ? null : (0, import_react39.createElement)(
"div",
{ key: "mode" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
- (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
+ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
),
- face || !usage ? null : (0, import_react38.createElement)(
+ face || !usage ? null : (0, import_react39.createElement)(
"div",
{ key: "usage" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, room2.name), (0, import_react38.createElement)("span", null, usage))
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Usage")),
+ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, room2.name), (0, import_react39.createElement)("span", null, usage))
)
];
}
@@ -15937,15 +15982,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_react38.useState)("details");
+ const [tab, setTab] = (0, import_react39.useState)("details");
const room2 = roster.roomsById[sessionId];
- const [view, setView] = (0, import_react38.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
- const [editing, setEditing] = (0, import_react38.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
- const [draftName, setDraftName] = (0, import_react38.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
- const [copied, setCopied] = (0, import_react38.useState)(false);
- const [error, setError] = (0, import_react38.useState)("");
+ const [view, setView] = (0, import_react39.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
+ const [editing, setEditing] = (0, import_react39.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
+ const [draftName, setDraftName] = (0, import_react39.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
+ const [copied, setCopied] = (0, import_react39.useState)(false);
+ const [error, setError] = (0, import_react39.useState)("");
const glide = useGlide(tab);
- const last = (0, import_react38.useRef)({ view, tab, move: void 0 });
+ const last = (0, import_react39.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;
@@ -15970,43 +16015,43 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
setTimeout(() => setCopied(false), 1600);
});
};
- const errorLine = error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ const errorLine = error ? (0, import_react39.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
if ((bot || room2) && view === "settings" && !bot?.remote) {
return [
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ key: "head", className: "bt-subhead", "data-move": move },
- (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react38.createElement)(ChevronLeftIcon)),
- (0, import_react38.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
- hasStrip ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react38.createElement)(CloseIcon))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react39.createElement)(ChevronLeftIcon)),
+ (0, import_react39.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
+ hasStrip ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react39.createElement)(CloseIcon))
),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
errorLine,
- bot ? (0, import_react38.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react38.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
+ bot ? (0, import_react39.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react39.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
)
];
}
- const top = (0, import_react38.createElement)(
+ const top = (0, import_react39.createElement)(
"div",
{ className: "bt-drawer-top", "data-move": side },
- bot || room2 ? (0, import_react38.createElement)("button", {
+ bot || room2 ? (0, import_react39.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_react38.createElement)(GearIcon)) : null,
- bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react38.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_react38.createElement)(GlobeIcon)) : null,
- bot || room2 ? (0, import_react38.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_react38.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react38.createElement)(ShareIcon)) : null,
- hasStrip ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react38.createElement)(CloseIcon))
+ }, (0, import_react39.createElement)(GearIcon)) : null,
+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react39.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_react39.createElement)(GlobeIcon)) : null,
+ bot || room2 ? (0, import_react39.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_react39.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react39.createElement)(ShareIcon)) : null,
+ hasStrip ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react39.createElement)(CloseIcon))
);
if (!bot && !room2) {
return [
- (0, import_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
- (0, import_react38.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react38.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
+ (0, import_react39.createElement)(import_react39.Fragment, { key: "top" }, top),
+ (0, import_react39.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react39.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
];
}
const current = bot?.name ?? room2.name;
@@ -16020,12 +16065,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_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(import_react39.Fragment, { key: "top" }, top),
+ (0, import_react39.createElement)(
"div",
{ key: "id", className: "bt-drawer-id", "data-move": side },
- bot ? (0, import_react38.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- bot?.remote ? (0, import_react38.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react38.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react38.createElement)("input", {
+ bot ? (0, import_react39.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react39.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
+ bot?.remote ? (0, import_react39.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react39.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react39.createElement)("input", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
@@ -16040,59 +16085,59 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
setEditing(false);
}
}
- }) : (0, import_react38.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
+ }) : (0, import_react39.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_react38.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react38.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
+ subtitle ? (0, import_react39.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react39.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
),
- bot?.remote ? null : (0, import_react38.createElement)(
+ bot?.remote ? null : (0, import_react39.createElement)(
"div",
{ key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side },
- (0, import_react38.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- tabs.map((key) => (0, import_react38.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
+ (0, import_react39.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ tabs.map((key) => (0, import_react39.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ key: `body:${tab}`, className: "bt-drawer-body", "data-move": move },
errorLine,
- room2 && tab === "details" ? (0, import_react38.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ room2 && tab === "details" ? (0, import_react39.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
bot && bot.remote && tab === "details" ? [
- bot.remote.handle ? (0, import_react38.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
- (0, import_react38.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
+ bot.remote.handle ? (0, import_react39.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
+ (0, import_react39.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
] : null,
bot && !bot.remote && tab === "details" ? [
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ key: "model", className: "bt-drawer-model" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Model")),
- (0, import_react38.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
- modelOf(bot, roster) ? null : (0, import_react38.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Model")),
+ (0, import_react39.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
+ modelOf(bot, roster) ? null : (0, import_react39.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
),
- (0, import_react38.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
+ (0, import_react39.createElement)(
"div",
{ key: "folder", className: "bt-drawer-model" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
- (0, import_react38.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
+ (0, import_react39.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
),
- bot.instructions ? (0, import_react38.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react38.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
- roster.memory === false ? null : (0, import_react38.createElement)(MemoryRow, { key: "memory", bot, actions }),
- usage ? (0, import_react38.createElement)(
+ bot.instructions ? (0, import_react39.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react39.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
+ roster.memory === false ? null : (0, import_react39.createElement)(MemoryRow, { key: "memory", bot, actions }),
+ usage ? (0, import_react39.createElement)(
"div",
{ key: "usage" },
- (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, bot.name), (0, import_react38.createElement)("span", null, usage))
+ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Usage")),
+ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, bot.name), (0, import_react39.createElement)("span", null, usage))
) : null,
- (0, import_react38.createElement)(ScheduleSummary, { key: "routines", bot, actions })
+ (0, import_react39.createElement)(ScheduleSummary, { key: "routines", bot, actions })
] : null,
- bot && !bot.remote && tab === "browser" ? (0, import_react38.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
+ bot && !bot.remote && tab === "browser" ? (0, import_react39.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
)
];
}
// src/client/browser-pane.js
-var import_react39 = require("react");
+var import_react40 = require("react");
var import_react_dom8 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
@@ -16102,10 +16147,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_react39.createElement)(
+ return (0, import_react40.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_react39.createElement)("path", { key: index, d }))
+ paths.map((d, index) => (0, import_react40.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"]);
@@ -16117,9 +16162,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_react39.useState)(null);
- if (!src || failed === src) return (0, import_react39.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react39.createElement)(GlobeIcon));
- return (0, import_react39.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
+ const [failed, setFailed] = (0, import_react40.useState)(null);
+ if (!src || failed === src) return (0, import_react40.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react40.createElement)(GlobeIcon));
+ return (0, import_react40.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
}
var savedWidth = () => {
try {
@@ -16178,11 +16223,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_react39.useRef)(null);
- const ind = (0, import_react39.useRef)(null);
- const spot = (0, import_react39.useRef)(null);
- const under = (0, import_react39.useRef)(null);
- (0, import_react39.useLayoutEffect)(() => {
+ const wrap = (0, import_react40.useRef)(null);
+ const ind = (0, import_react40.useRef)(null);
+ const spot = (0, import_react40.useRef)(null);
+ const under = (0, import_react40.useRef)(null);
+ (0, import_react40.useLayoutEffect)(() => {
const box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
@@ -16211,7 +16256,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
trail.cancel();
};
});
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
@@ -16225,13 +16270,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
observer.observe(box);
return () => observer.disconnect();
}, []);
- return (0, import_react39.createElement)(
+ return (0, import_react40.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
"button",
{
type: "button",
@@ -16241,13 +16286,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
title: bot.name,
onClick: () => onView("details")
},
- (0, import_react39.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
- view === "details" ? (0, import_react39.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
+ (0, import_react40.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
+ view === "details" ? (0, import_react40.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_react39.createElement)(
+ return (0, import_react40.createElement)(
"button",
{
key: tabId,
@@ -16268,9 +16313,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}
}
},
- (0, import_react39.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
- (0, import_react39.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
- (0, import_react39.createElement)("span", {
+ (0, import_react40.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
+ (0, import_react40.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
+ (0, import_react40.createElement)("span", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
@@ -16282,20 +16327,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}, "×")
);
}),
- (0, import_react39.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
+ (0, import_react40.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_react39.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react39.createElement)(PlusTabIcon)),
- (0, import_react39.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_react39.createElement)(CloseIcon2))
+ (0, import_react40.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react40.createElement)(PlusTabIcon)),
+ (0, import_react40.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_react40.createElement)(CloseIcon2))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE;
- const frame = (0, import_react39.useRef)(null);
- const input = (0, import_react39.useRef)(null);
- const [draft, setDraft] = (0, import_react39.useState)(null);
+ const frame = (0, import_react40.useRef)(null);
+ const input = (0, import_react40.useRef)(null);
+ const [draft, setDraft] = (0, import_react40.useState)(null);
const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react39.useLayoutEffect)(() => {
+ (0, import_react40.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
@@ -16321,7 +16366,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
window.removeEventListener("resize", schedule);
};
}, [tabId, showGuest, leaving]);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
@@ -16337,27 +16382,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
};
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react39.createElement)(
+ return (0, import_react40.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ className: "bt-browser-bar" },
- (0, import_react39.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_react39.createElement)(BackIcon)),
- (0, import_react39.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_react39.createElement)(ForwardIcon)),
- (0, import_react39.createElement)("button", {
+ (0, import_react40.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_react40.createElement)(BackIcon)),
+ (0, import_react40.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_react40.createElement)(ForwardIcon)),
+ (0, import_react40.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_react39.createElement)(page.loading ? StopIcon : ReloadIcon)),
- (0, import_react39.createElement)(
+ }, (0, import_react40.createElement)(page.loading ? StopIcon : ReloadIcon)),
+ (0, import_react40.createElement)(
"form",
{ className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 },
- (0, import_react39.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react39.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
- (0, import_react39.createElement)("input", {
+ (0, import_react40.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react40.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
+ (0, import_react40.createElement)("input", {
ref: input,
type: "text",
spellCheck: false,
@@ -16381,25 +16426,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
}
})
),
- page.loading ? (0, import_react39.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
+ page.loading ? (0, import_react40.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
),
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react39.createElement)(
+ page.error ? (0, import_react40.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
- (0, import_react39.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
- page.url ? (0, import_react39.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
+ (0, import_react40.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
+ (0, import_react40.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
+ page.url ? (0, import_react40.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
void actions.browserNavigate(tabId, page.url);
} }, t("Try again")) : null
- ) : !page.url ? (0, import_react39.createElement)(
+ ) : !page.url ? (0, import_react40.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react39.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
- (0, import_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
- (0, import_react39.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_react40.createElement)(BotMark, { bot, size: 48, 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-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
)
);
@@ -16411,41 +16456,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_react39.useRef)(null);
- const fly = (0, import_react39.useRef)(null);
- const [landed, setLanded] = (0, import_react39.useState)(false);
- const [settling, setSettling] = (0, import_react39.useState)(false);
- const [width, setWidth] = (0, import_react39.useState)(savedWidth);
- const [viewport, setViewport] = (0, import_react39.useState)(() => window.innerWidth);
- const [dragging, setDragging] = (0, import_react39.useState)(false);
- const [resizing, setResizing] = (0, import_react39.useState)(false);
+ const root = (0, import_react40.useRef)(null);
+ const fly = (0, import_react40.useRef)(null);
+ const [landed, setLanded] = (0, import_react40.useState)(false);
+ const [settling, setSettling] = (0, import_react40.useState)(false);
+ const [width, setWidth] = (0, import_react40.useState)(savedWidth);
+ const [viewport, setViewport] = (0, import_react40.useState)(() => window.innerWidth);
+ const [dragging, setDragging] = (0, import_react40.useState)(false);
+ const [resizing, setResizing] = (0, import_react40.useState)(false);
const instant = dragging || resizing;
- const [reading, setReading] = (0, import_react39.useState)(false);
+ const [reading, setReading] = (0, import_react40.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react39.useLayoutEffect)(() => {
+ (0, import_react40.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react39.useLayoutEffect)(() => {
+ (0, import_react40.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react39.useState)({ view, width: panelWidth });
+ const [last, setLast] = (0, import_react40.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_react39.useRef)(view);
- (0, import_react39.useLayoutEffect)(() => {
+ const shownView = (0, import_react40.useRef)(view);
+ (0, import_react40.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_react39.useEffect)(() => () => {
+ (0, import_react40.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
@@ -16459,28 +16504,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
window.removeEventListener("resize", onResize);
};
}, []);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
if (!settling) return void 0;
const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40);
return () => clearTimeout(timer);
}, [settling, view]);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react39.useEffect)(() => () => {
+ (0, import_react40.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react39.useEffect)(() => {
+ (0, import_react40.useEffect)(() => {
if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
setReading(true);
const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -16517,7 +16562,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
return actions[name](...args);
};
}
- (0, import_react39.useLayoutEffect)(() => {
+ (0, import_react40.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
@@ -16600,7 +16645,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_react39.createElement)(
+ return (0, import_react40.createElement)(
"aside",
{
ref: root,
@@ -16617,15 +16662,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
if (event.target === event.currentTarget) setLanded(true);
}
},
- leaving ? null : (0, import_react39.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
+ leaving ? null : (0, import_react40.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_react39.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
- hasStrip ? (0, import_react39.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
- view === "browser" && bot ? (0, import_react39.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react39.createElement)(
+ dragging ? (0, import_react_dom8.createPortal)((0, import_react40.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
+ hasStrip ? (0, import_react40.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
+ view === "browser" && bot ? (0, import_react40.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react40.createElement)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react39.createElement)(DetailsDrawer, {
+ (0, import_react40.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
@@ -16722,7 +16767,7 @@ var BROWSER_CSS = `
`;
// src/client/remote-bot-settings.js
-var import_react40 = require("react");
+var import_react41 = 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}
@@ -16746,8 +16791,8 @@ var REMOTE_BOT_SETTINGS_CSS = `
function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) {
const close = () => actions.closeSettings();
useEscape(close);
- const dialog = (0, import_react40.useRef)(null);
- (0, import_react40.useEffect)(() => {
+ const dialog = (0, import_react41.useRef)(null);
+ (0, import_react41.useEffect)(() => {
const before = document.activeElement;
dialog.current?.focus?.({ preventScroll: true });
return () => {
@@ -16762,37 +16807,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_react40.createElement)(
+ return (0, import_react41.createElement)(
"div",
{ className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
- (0, import_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react41.createElement)(
"div",
{ className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-remote-sheet-head" },
- (0, import_react40.createElement)(BotMark, { bot, size: 40, live: true }),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(BotMark, { bot, size: 40, live: true }),
+ (0, import_react41.createElement)(
"div",
{ className: "bt-remote-sheet-who" },
- (0, import_react40.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
- (0, import_react40.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
+ (0, import_react41.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
+ (0, import_react41.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
),
- (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react40.createElement)(CloseIcon))
+ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon))
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-remote-sheet-body" },
- bot.remote.handle ? (0, import_react40.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
- (0, import_react40.createElement)(FlyBotDetails, { bot, roster, actions })
+ bot.remote.handle ? (0, import_react41.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
+ (0, import_react41.createElement)(FlyBotDetails, { bot, roster, actions })
)
)
);
}
// src/client/overlays.js
-var import_react41 = require("react");
+var import_react42 = require("react");
var import_react_dom9 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
@@ -16801,15 +16846,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_react41.useRef)(current);
- (0, import_react41.useEffect)(() => {
+ const previous = (0, import_react42.useRef)(current);
+ (0, import_react42.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_react41.useState)(open);
- (0, import_react41.useEffect)(() => {
+ const [kept, setKept] = (0, import_react42.useState)(open);
+ (0, import_react42.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
@@ -16825,20 +16870,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_react41.createElement)(
- import_react41.Fragment,
+ return (0, import_react42.createElement)(
+ import_react42.Fragment,
null,
- pane ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
- ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
- ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
- menu ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
- remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
- inbox ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
- board ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
+ pane ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
+ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
+ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
+ menu ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
+ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
+ inbox ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
+ board ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
);
}
@@ -17792,7 +17837,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
}
// src/client/user-message.js
-var import_react42 = require("react");
+var import_react43 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
@@ -17878,38 +17923,38 @@ function userTextParts(text, bots, referenceLabels = []) {
return parts;
}
function UserText({ text, bots, referenceLabels, skillNames, references, actions }) {
- const chip = (part) => [(0, import_react42.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
+ const chip = (part) => [(0, import_react43.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_react42.createElement)(import_react42.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ if (!part.bot) return (0, import_react43.createElement)(import_react43.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
const title = `@${part.bot.name}`;
- return typeof part.bot.id === "string" ? (0, import_react42.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react42.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
+ return typeof part.bot.id === "string" ? (0, import_react43.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react43.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
});
}
function Attachments({ attachments, renderMessageImages }) {
const compact2 = attachments.length > 1;
- return (0, import_react42.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
+ return (0, import_react43.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
if (block.type === "image") {
- return renderMessageImages ? (0, import_react42.createElement)(import_react42.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null;
+ return renderMessageImages ? (0, import_react43.createElement)(import_react43.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_react42.createElement)(
+ return (0, import_react43.createElement)(
"span",
{ key: index, className: "bt-ufile", title: file.name },
- ui.FileTypeIcon ? (0, import_react42.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
- (0, import_react42.createElement)("span", { className: "bt-ufile-text" }, (0, import_react42.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react42.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
+ ui.FileTypeIcon ? (0, import_react43.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
+ (0, import_react43.createElement)("span", { className: "bt-ufile-text" }, (0, import_react43.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react43.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
);
}));
}
function CopyButton({ text }) {
- const [copied, setCopied] = (0, import_react42.useState)(false);
+ const [copied, setCopied] = (0, import_react43.useState)(false);
const copy2 = () => {
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1e3);
});
};
- return (0, import_react42.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_react42.createElement)(CheckIcon) : (0, import_react42.createElement)(CopyIcon));
+ return (0, import_react43.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_react43.createElement)(CheckIcon) : (0, import_react43.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 }) {
@@ -17926,7 +17971,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_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{
className: incoming ? "bt-umsg bt-umsg-other" : "bt-umsg",
@@ -17936,22 +17981,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma
"data-author-kind": author?.authorKind,
"data-author-name": author?.name
},
- person ? (0, import_react42.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react42.createElement)(PersonMark, { name: author.name })) : null,
- faceMine ? (0, import_react42.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react42.createElement)(PersonMark, { name: myName || void 0 })) : null,
- (0, import_react42.createElement)(
+ person ? (0, import_react43.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react43.createElement)(PersonMark, { name: author.name })) : null,
+ faceMine ? (0, import_react43.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react43.createElement)(PersonMark, { name: myName || void 0 })) : null,
+ (0, import_react43.createElement)(
"div",
{ className: "bt-ustack" },
- faceMine && myName ? (0, import_react42.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react42.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react42.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
- attachments.length > 0 ? (0, import_react42.createElement)(Attachments, { attachments, renderMessageImages }) : null,
- text !== "" || rest.length > 0 ? (0, import_react42.createElement)(
+ faceMine && myName ? (0, import_react43.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react43.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
+ attachments.length > 0 ? (0, import_react43.createElement)(Attachments, { attachments, renderMessageImages }) : null,
+ text !== "" || rest.length > 0 ? (0, import_react43.createElement)(
"div",
{ className: "bt-ubody", title: clock3(data.time) },
- (0, import_react42.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_react42.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
+ (0, import_react43.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_react43.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
) : null,
- referenceLabels.length > 0 ? (0, import_react42.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
+ referenceLabels.length > 0 ? (0, import_react43.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
),
- text === "" ? null : (0, import_react42.createElement)("div", { className: "bt-utools" }, (0, import_react42.createElement)(CopyButton, { text }))
+ text === "" ? null : (0, import_react43.createElement)("div", { className: "bt-utools" }, (0, import_react43.createElement)(CopyButton, { text }))
);
}
var USER_MESSAGE_CSS = `
@@ -18049,7 +18094,7 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since
}
// src/client/stats-dock.js
-var import_react43 = require("react");
+var import_react44 = require("react");
var STATS_DOCK = "conversation.composer.dock";
var STATS_ID = "stats";
var STATS_IDS = [STATS_ID, "activity"];
@@ -18065,16 +18110,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_react43.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
+ const t2 = (0, import_react44.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_react43.createElement)(entry.component, { ...props, t: t2 });
+ return (0, import_react44.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_react45 = require("react");
var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"];
function apply(ctx) {
const flybot = createClientApi2();
@@ -19020,6 +19066,14 @@ function apply(ctx) {
shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock),
shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock)
];
+ ctx.effect(() => {
+ 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_react45.createElement)(TurnFailures, { ...props, work }) });
+ return () => {
+ off();
+ style.remove();
+ };
+ }, "ds-bot: dead turn lines");
ctx.effect(() => {
const style = installStyles(RETURN_CSS, "return");
return () => style.remove();
diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js
index 53beb745f..f50197602 100644
--- a/core/src/client/i18n.js
+++ b/core/src/client/i18n.js
@@ -27,6 +27,10 @@ const ZH = {
'Enter a number, or paste a group invite link': '输入数字面对面建群,或粘贴邀请链接加入',
'Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.': '选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。',
'Or paste a group invite link (https://…/r/…)': '或粘贴群邀请链接(https://…/r/…)',
+ // #1044: a turn that died before writing anything still says so where the chat is.
+ 'Could not run: {error}': '这一轮没能跑起来:{error}',
+ 'the model call failed': '模型调用失败',
+ '{count} turns in a row': '连续 {count} 轮',
'Group invite link': '群邀请链接',
'Room-only: friends do not see these Bots unless you invite them.': '仅限这个房间:除非你邀请,好友看不到这些 Bot。',
'6 digits': '6 位数字',
diff --git a/core/src/client/index.js b/core/src/client/index.js
index 092ce3f70..e13998a99 100644
--- a/core/src/client/index.js
+++ b/core/src/client/index.js
@@ -1,4 +1,7 @@
import { FLYBOT_CSS, FlyBotApprovals, FlyBotSessionDock, createClientApi as createFlyBotClient } from './flybot-slots.js'
+import { createElement as h } from 'react'
+import { registry as flybotSlots } from './flybot-registry.js'
+import { TURN_FAILURES_CSS, TurnFailures } from './turn-failures.js'
import { call } from './api.js'
import { isHex } from './inks.js'
import { SHAPES, markupCache } from './characters.js'
@@ -932,6 +935,12 @@ export function apply(ctx) {
shadow('conversation.input.dock', { id: 'bot-secret', order: -90, inject: face }, SecretDock),
shadow('conversation.input.dock', { id: 'flybot-session', order: -80, inject: face }, FlyBotSessionDock),
]
+ // A turn that died before writing a word says so above the composer, in every chat (#1044).
+ ctx.effect(() => {
+ const style = installStyles(TURN_FAILURES_CSS, 'turn-failures')
+ const off = flybotSlots().register('flybot.session.dock', { id: 'flybot-turn-failures', order: 20, component: props => h(TurnFailures, { ...props, work }) })
+ return () => { off(); style.remove() }
+ }, 'ds-bot: dead turn lines')
// The return button belongs to neither group: it renders only in Agent mode.
ctx.effect(() => {
const style = installStyles(RETURN_CSS, 'return')
diff --git a/core/src/client/turn-failures.js b/core/src/client/turn-failures.js
new file mode 100644
index 000000000..0c1019521
--- /dev/null
+++ b/core/src/client/turn-failures.js
@@ -0,0 +1,36 @@
+// A turn that dies before writing a single word leaves the chat looking like a Bot that read the
+// reminder and chose to stay quiet (#1044): on 2026-10-24 eight beats in a row were dead for three
+// and a half hours because a provider budget cap refused every model call and nothing was drawn.
+//
+// The host already knows: `noteEnded` keeps the newest turns and marks a failure that produced no text
+// `silent`, with the streak at the same place. This draws those lines above the composer, one per turn,
+// newest last. It never posts, never retries and never counts as a reply, so a dead beat is visible while
+// the audience rules see exactly what they saw before.
+import { createElement as h, useEffect, useState } from 'react'
+import { t } from './i18n.js'
+import { failureLines } from '../shared/turn-failures.js'
+
+export { failureLines }
+
+export function TurnFailures({ sessionId, work }) {
+ const [snapshot, setSnapshot] = useState(() => work?.getSnapshot?.())
+ useEffect(() => {
+ if (typeof work?.subscribe !== 'function') return undefined
+ const read = () => setSnapshot(work.getSnapshot())
+ const off = work.subscribe(read)
+ read()
+ return off
+ }, [work])
+ const lines = failureLines(snapshot, sessionId, t)
+ if (lines.length === 0) return null
+ return h('div', { className: 'bt-turn-failures', 'data-flybot': 'turn-failures' },
+ lines.map(line => h('div', { key: line.id, className: 'bt-turn-failure', 'data-flybot': 'turn-failure' },
+ h('span', { className: 'bt-turn-failure-text' }, line.text),
+ line.streak === undefined ? null : h('span', { className: 'bt-turn-failure-streak' }, ` ${t('{count} turns in a row', { count: line.streak })}`))))
+}
+
+export const TURN_FAILURES_CSS = `
+.bt-turn-failures{width:100%;max-width:min(992px,100%);margin:0 auto;padding:0 0 6px;box-sizing:border-box}
+.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}
+`
diff --git a/core/src/host/turns.js b/core/src/host/turns.js
index b3fa7069b..9655f3fc5 100644
--- a/core/src/host/turns.js
+++ b/core/src/host/turns.js
@@ -329,7 +329,10 @@ export function install(rt) {
.filter(entry => entry.failed !== undefined && latest.get(placeOfEnded(entry)) === entry && now - entry.at <= FAILED_MS)
return [...older.slice(-FAILED_KEPT), ...list.slice(-ENDED_KEPT)]
}
- const noteEnded = (id, turn, info, reason, failure, calls) => {
+ // A turn that died before writing a line leaves the chat looking like a Bot that chose to stay
+ // quiet (#1044), so a failure with no text is marked `silent` and carries the streak of them at the
+ // same place: one blip and a provider that has been refusing for hours read differently.
+ const noteEnded = (id, turn, info, reason, failure, calls, texts = 0) => {
if (failure === undefined && reason?.kind !== 'completed') return
const owner = rt.groupOwners.get(id)
const bot = rt.botOf(owner?.botId ?? rt.selfOf(id))
@@ -338,6 +341,10 @@ export function install(rt) {
if (owner === undefined && state.exchangeTurns?.[id]?.includes(turn)) return
if (owner !== undefined && rt.roomOf(owner.roomId)?.origin !== undefined) return
const at = Date.now()
+ const place = `${bot.id}|${owner?.roomId ?? ''}`
+ const silent = failure !== undefined && texts === 0
+ const lastHere = ended.findLast?.(entry => placeOfEnded(entry) === place) ?? [...ended].reverse().find(entry => placeOfEnded(entry) === place)
+ const streak = silent ? (lastHere?.silent === true ? (lastHere.streak ?? 0) + 1 : 1) : 0
ended = trimEnded([...ended, {
id: `${id}:${turn}:${at}`,
botId: bot.id,
@@ -345,6 +352,7 @@ export function install(rt) {
at,
...(calls > 0 ? { calls } : {}),
...(failure ? { failed: (failure.message || failure.code).slice(0, 160) } : {}),
+ ...(silent ? { silent: true, streak } : {}),
}], at)
}
const endedView = () => ended
@@ -564,7 +572,7 @@ export function install(rt) {
const wroteCall = failure === undefined && reason?.kind === 'completed' && lastCalled !== true && writesToolCall(lastText)
const retry = wroteCall && info?.retried !== true && !(info?.role === 'room' && stoppedRoomTurns.has(info.exchangeId))
if (retry) void retryWrittenCall(id, info, origin).catch(error => rt.warn('ds-bot: asking %s to run its written tool call failed: %s', id, error))
- if (!retry) noteEnded(id, answeredTurn, info, reason, failure, toolCalls)
+ if (!retry) noteEnded(id, answeredTurn, info, reason, failure, toolCalls, texts.length)
const replyText = () => {
const reply = (wroteCall ? texts.map(stripWrittenCalls) : texts).join('\n\n').trim()
return wroteCall && reply === '' ? 'Tried to run a tool but wrote the call as text, so it did not run.' : reply
diff --git a/core/src/shared/turn-failures.js b/core/src/shared/turn-failures.js
new file mode 100644
index 000000000..7ae5d2696
--- /dev/null
+++ b/core/src/shared/turn-failures.js
@@ -0,0 +1,17 @@
+// What a chat says about turns that died before writing a word (#1044). The host marks those `ended`
+// entries `silent` and counts the streak at the same place; this turns the snapshot the client already
+// has into the lines a chat draws. Kept free of React so both the bundle and the tests can read it.
+
+export const FAILURE_LINES_SHOWN = 3
+
+export const failureLines = (snapshot, place, t, limit = FAILURE_LINES_SHOWN) => {
+ const ended = Array.isArray(snapshot?.ended) ? snapshot.ended : []
+ return ended
+ .filter(entry => entry?.silent === true && (entry.roomId ?? entry.sessionId) === place)
+ .slice(-limit)
+ .map(entry => ({
+ id: `turn-failure-${entry.id}`,
+ text: t('Could not run: {error}', { error: entry.failed ?? t('the model call failed') }),
+ ...(Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {}),
+ }))
+}
diff --git a/core/test/approval-notices.test.js b/core/test/approval-notices.test.js
index a02c726d6..ad73cf810 100644
--- a/core/test/approval-notices.test.js
+++ b/core/test/approval-notices.test.js
@@ -251,8 +251,8 @@ test('the host lists finished and failed turns of my own Bots, not stopped, aske
turn('g2', 1, completed, { kind: SOURCE_KIND, role: 'room', exchangeId: 'r2x' })
assert.deepEqual(rt.endedView().map(({ id, at, ...entry }) => [id.split(':').slice(0, 2).join(':'), entry]), [
['b:1', { botId: 'b', sessionId: 'b' }],
- ['b:2', { botId: 'b', sessionId: 'b', failed: 'upstream busy' }],
- ['g1:1', { botId: 'b', roomId: 'r1', failed: 'upstream busy' }],
+ ['b:2', { botId: 'b', sessionId: 'b', failed: 'upstream busy', silent: true, streak: 1 }],
+ ['g1:1', { botId: 'b', roomId: 'r1', failed: 'upstream busy', silent: true, streak: 1 }],
])
for (let n = 10; n < 40; n++) turn('b', n, completed)
// Past the last 20, the failure in r1 stays: it is still the latest turn there (#471).
diff --git a/core/test/dead-turn.test.js b/core/test/dead-turn.test.js
new file mode 100644
index 000000000..1ad667436
--- /dev/null
+++ b/core/test/dead-turn.test.js
@@ -0,0 +1,140 @@
+import assert from 'node:assert/strict'
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { test } from 'node:test'
+import { apply } from '../index.js'
+import { fakeContext } from './fake-ctx.js'
+import { failureLines } from '../src/shared/turn-failures.js'
+
+// #1044: a turn that dies on a provider refusal without writing a word must leave one visible line in
+// the chat, and three in a row must stay three lines with the streak on them. The host half is what the
+// tests below read (the `work` snapshot the client draws); the client half is `failureLines`.
+
+const seed = () => ({
+ version: 1,
+ mainBotIds: ['a'],
+ bots: {
+ a: { id: 'a', name: 'Chief', role: 'Boss', sessionId: 'a', createdAt: 1 },
+ b: { id: 'b', name: 'Writer', role: 'Write', sessionId: 'b', createdAt: 2 },
+ },
+ rooms: {},
+ agents: {},
+ exchanges: [],
+ exchangeTurns: {},
+ questions: {},
+ prefs: {},
+ revision: 0,
+})
+
+async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-dead-turn-'))) {
+ if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed()))
+ const ctx = fakeContext()
+ const agents = new Map()
+ const agentOf = (sessionId) => {
+ if (!agents.has(sessionId)) {
+ const calls = []
+ const held = []
+ const inbox = { nextStep: [], nextTurn: held, remove() { return false } }
+ const keep = (message) => { calls.push(message); held.push(message) }
+ agents.set(sessionId, { session: { id: sessionId }, calls, cancelled: [], inbox, followup: keep, steer: keep, cancel() {} })
+ }
+ return agents.get(sessionId)
+ }
+ Object.assign(ctx, {
+ llm: { listProviders: () => [], listModels: async () => [], resolveModelInfo: async () => ({}) },
+ workspaceRegistry: { get: () => undefined, resolveByPath: async () => undefined, create: async path => ({ id: 'w1', path }), archiveSession: async () => {} },
+ sessionController: { create: async () => ({ sessionId: 's1' }), resolveAgent: async sessionId => ({ agent: agentOf(sessionId) }), selectModel: async () => {} },
+ sessionTitle: { rename: async () => {} },
+ sessions: { flush: async () => {} },
+ })
+ let route
+ const register = ctx.connection.fetch.register
+ ctx.connection.fetch.register = (options) => { route = options; return register(options) }
+ apply(ctx, { home })
+ let seq = 0
+ const emit = (id, type, data) => {
+ for (const listener of ctx.listeners.get('session/event') ?? []) {
+ if (listener.length >= 2) listener({ id }, { type, seq: ++seq, data })
+ }
+ }
+ const tool = (name, args, sessionId) => ctx.registeredTools.get(name).execute(args, { agent: { id: sessionId } })
+ const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json()
+ const ended = async () => (await call('state', {})).value.work.ended ?? []
+ const stop = async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }
+ await tool('list_bots', {}, 'a')
+ return { emit, call, ended, stop }
+}
+
+const budget429 = {
+ code: 'PROVIDER_ERROR',
+ status: 429,
+ message: '429: {"message":"Budget has been exceeded! Key=00885737 Current cost: 300.0019044899996, Max budget: 300.0","type":"budget_exceeded"}',
+}
+
+const die = (emit, id, turn, status = budget429, texts = 0) => {
+ emit(id, 'turn/start', { turn })
+ for (let n = 0; n < texts; n++) emit(id, 'assistant/message', { turn, message: { content: [{ type: 'text', text: `line ${n}` }] } })
+ emit(id, 'turn/end', { turn, reason: { kind: 'error', error: status } })
+}
+
+test('#1044: a turn that dies with no text is marked silent, once, naming the provider error', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ const [entry] = await host.ended()
+ assert.equal(entry.silent, true)
+ assert.equal(entry.streak, 1)
+ assert.match(entry.failed, /Budget has been exceeded/)
+ const lines = failureLines({ ended: await host.ended() }, 'b', (key, values) => (values?.error === undefined ? key : `${key.split('{error}')[0]}${values.error}`))
+ assert.equal(lines.length, 1)
+ assert.match(lines[0].text, /Budget has been exceeded/)
+ assert.equal(lines[0].streak, undefined)
+})
+
+test('#1044: a turn that wrote text then failed adds no line', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1, budget429, 2)
+ const [entry] = await host.ended()
+ assert.equal(entry.silent, undefined)
+ assert.equal(failureLines({ ended: await host.ended() }, 'b', key => key).length, 0)
+})
+
+test('#1044: three dead turns in a row are three lines and the streak is visible', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ die(host.emit, 'b', 2)
+ die(host.emit, 'b', 3)
+ const ended = await host.ended()
+ assert.deepEqual(ended.map(entry => entry.silent), [true, true, true])
+ assert.deepEqual(ended.map(entry => entry.streak), [1, 2, 3])
+ const lines = failureLines({ ended }, 'b', (key, values) => `${key}:${JSON.stringify(values ?? {})}`)
+ assert.equal(lines.length, 3)
+ assert.deepEqual(lines.map(line => line.streak), [undefined, 2, 3])
+})
+
+test('#1044: a successful turn after the dead ones resets the streak', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ die(host.emit, 'b', 2)
+ host.emit('b', 'turn/start', { turn: 3 })
+ host.emit('b', 'assistant/message', { turn: 3, message: { content: [{ type: 'text', text: 'ok' }] } })
+ host.emit('b', 'turn/end', { turn: 3, reason: { kind: 'completed' } })
+ die(host.emit, 'b', 4)
+ const ended = await host.ended()
+ assert.deepEqual(ended.map(entry => [entry.silent ?? null, entry.streak ?? null]), [[true, 1], [true, 2], [null, null], [true, 1]])
+})
+
+test('#1044: the line is not a message: the dead turn adds nothing to the chat', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ const state = (await host.call('state', {})).value
+ const lines = failureLines({ ended: state.work.ended }, 'b', key => key)
+ assert.equal(lines.length, 1)
+ // Nothing was delivered to anyone for it, and the entry the notifications read is unchanged.
+ assert.deepEqual(state.work.bots, {})
+})
diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md
index 6652baffe..a9e74a182 100644
--- a/docs/subsystems/core-client.md
+++ b/docs/subsystems/core-client.md
@@ -37,6 +37,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl
| `core/src/shared/held-retry.js` | The one rule for "this failure can still be retried" (#968): `canRetry(record)` is false for `retry: false`, for `noRetry: true`, and for a record whose stored `error`/`code` names a terminal reason (`no such remote bot` / `is not shared with your Bots` / `is no longer shared with you` / `has not granted chat.public` / `bot_not_shared` / `bot_not_found`; a bare `capability_not_granted` is not one, #988), true otherwise. The host states the same answer in two spellings (`noRetry` on an approval, `retry: false` on a held reply), so both cards and Bot Social's own dock ask this one function instead of keeping a copy each; a hold written before the flag existed is read from its stored sentence at render time, with no state migration. Tests: `core/test/held-retry.test.js`. |
| `core/src/client/schedules.js` | Settings → Scheduled tasks, a Bot's card and the details line. Each row has **Edit** (pencil; the row turns into the same form that creates a task, filled in, with **Save changes**; an edited task keeps its time zone, a cron rule it cannot show is kept) and **Pause** / **Resume**; a paused row reads **Paused** (#354). A row with a recorded run adds one line under it: "Running since …", "Last run …", "Last run … · Stopped" or, in red, "Last run … · Failed: "; a failed run of a task that is not paused gets **Retry**, which runs the task once now (`actions.scheduleRetry`, #473). With Automation tasks off, the hint reads "Switch on Automation tasks on the Plugins page." and the button **Open Plugins** closes the Settings dialog and opens DSH's Plugins panel (`actions.selectPanel('plugins')`); the Harness settings dialog has no such switch (#378). Tests: `core/test/schedules-client.test.js`. |
| `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); a group's open question card (#552). 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/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. 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. 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/docs/subsystems/core-host.md b/docs/subsystems/core-host.md
index fa3cad25c..2fede8cb1 100644
--- a/docs/subsystems/core-host.md
+++ b/docs/subsystems/core-host.md
@@ -256,7 +256,7 @@ Work a member hands a Bot with `message_bot` from its group Session runs in a hi
### Ended turns for notifications (#353)
-- `turns.js` keeps `endedView()` (`trimEnded`): the last 20 turns, plus, past those, each place's (Bot + group) latest turn while it is a failure younger than 7 days (at most 50, for the Agent inbox's failed turns, #471), of the user's own Bots that finished (`reason.kind === 'completed'`) or failed (`failed`: the failure's message, else its code, at most 160 characters; an AUTH message stays out as in `turnFailure`), as `{ id: '::', botId, sessionId | roomId, at, calls? }` (`calls` is that turn's tool-call count, #471; left out for 0). Process-local, never saved. Not listed: a turn stopped, switched to a new part or retried for a tool call written as text; a friend's Bot (`bot.remote`); a turn a friend started (`fromRemote`, a remote-chat line); a hidden turn another Bot asked for (`exchangeTurns`); a group turn in a room with an `origin` (face-to-face, inbound-share). It rides `live().work.ended`; `rt.ownedLive` keeps the actor's own Bots' entries. `set-prefs` takes `notifications: false` (kept) or `true` / `null` (cleared: on by default), and `notify: { approval | finished | failed: boolean }` (#469: merged into `prefs.notify`, only the `false` ones kept, removed when empty). The client's use is in [core-client](core-client.md#system-notifications-353).
+- `turns.js` keeps `endedView()` (`trimEnded`): the last 20 turns, plus, past those, each place's (Bot + group) latest turn while it is a failure younger than 7 days (at most 50, for the Agent inbox's failed turns, #471), of the user's own Bots that finished (`reason.kind === 'completed'`) or failed (`failed`: the failure's message, else its code, at most 160 characters; an AUTH message stays out as in `turnFailure`), as `{ id: '::', botId, sessionId | roomId, at, calls?, silent?, streak? }` (`calls` is that turn's tool-call count, #471; left out for 0; a failure that produced no assistant text at all is `silent: true` and carries `streak`, how many silent turns in a row at that place, so a reader can tell one blip from a provider that has been refusing for hours, #1044 — a turn that wrote text and then failed is not silent, and a finished turn resets the streak). Process-local, never saved. Not listed: a turn stopped, switched to a new part or retried for a tool call written as text; a friend's Bot (`bot.remote`); a turn a friend started (`fromRemote`, a remote-chat line); a hidden turn another Bot asked for (`exchangeTurns`); a group turn in a room with an `origin` (face-to-face, inbound-share). It rides `live().work.ended`; `rt.ownedLive` keeps the actor's own Bots' entries. `set-prefs` takes `notifications: false` (kept) or `true` / `null` (cleared: on by default), and `notify: { approval | finished | failed: boolean }` (#469: merged into `prefs.notify`, only the `false` ones kept, removed when empty). The client's use is in [core-client](core-client.md#system-notifications-353).
### A burst of the user's lines (#642)
diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch
index 9c2754ef2..c57a6fa72 100644
--- a/patches/ds-bot/0001-core-fixes.patch
+++ b/patches/ds-bot/0001-core-fixes.patch
@@ -464,7 +464,7 @@ diff --git a/src/client/feedback.js b/src/client/feedback.js
diff --git a/src/client/i18n.js b/src/client/i18n.js
--- a/src/client/i18n.js
+++ b/src/client/i18n.js
-@@ -12,6 +12,59 @@ const ZH = {
+@@ -12,6 +12,63 @@
'Unread activity': '有未读',
Search: '搜索',
'New chat': '新对话',
@@ -483,6 +483,10 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
+ 'Enter a number, or paste a group invite link': '输入数字面对面建群,或粘贴邀请链接加入',
+ 'Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.': '选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。',
+ 'Or paste a group invite link (https://…/r/…)': '或粘贴群邀请链接(https://…/r/…)',
++ // #1044: a turn that died before writing anything still says so where the chat is.
++ 'Could not run: {error}': '这一轮没能跑起来:{error}',
++ 'the model call failed': '模型调用失败',
++ '{count} turns in a row': '连续 {count} 轮',
+ 'Group invite link': '群邀请链接',
+ 'Room-only: friends do not see these Bots unless you invite them.': '仅限这个房间:除非你邀请,好友看不到这些 Bot。',
+ '6 digits': '6 位数字',
@@ -524,7 +528,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'New…': '新建',
'Animation': '动画',
'Bot animation': 'Bot 动画',
-@@ -26,6 +26,21 @@ const ZH = {
+@@ -26,6 +83,21 @@
Duplicate: '复制一份',
Hidden: '已隐藏',
Admin: '管理员',
@@ -546,7 +550,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Add: '添加',
Close: '关闭',
Copy: '复制',
-@@ -41,22 +41,69 @@ const ZH = {
+@@ -41,22 +113,69 @@
' and ': '、',
and: '和',
'{count} members': '{count} 位成员',
@@ -619,7 +623,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Connectors: '连接器',
'Search connectors': '搜索连接器',
'No connectors': '暂无连接器',
-@@ -93,6 +93,59 @@ const ZH = {
+@@ -93,6 +212,59 @@
'Connecting…': '正在连接…',
'Could not connect: {error}': '连接失败:{error}',
'Connected as {account}': '已连接:{account}',
@@ -679,7 +683,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Connected: '已连接',
Disconnect: '断开',
Connect: '连接',
-@@ -105,17 +105,44 @@ const ZH = {
+@@ -105,17 +277,44 @@
'GitHub addresses must start with https://': 'GitHub 地址必须以 https:// 开头',
'This DSH cannot load connectors': '这个 DSH 不能加载连接器',
'No such connector': '没有这个连接器',
@@ -728,7 +732,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Header
'Message {name}': '给 {name} 发消息',
-@@ -128,8 +128,6 @@ const ZH = {
+@@ -128,8 +327,6 @@
New: '新',
// New chat
@@ -737,7 +741,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Add to group chat': '加入群聊',
'Message Bot': '给 Bot 发消息',
'To:': '发给:',
-@@ -154,9 +154,20 @@ const ZH = {
+@@ -154,9 +351,20 @@
Everyone: '所有人',
'Every member answers in turn, admin first.': '每位成员轮流回答,管理员先说。',
'Members · {count}': '成员 · {count}',
@@ -758,7 +762,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Remove {name} from the group': '把 {name} 移出群聊',
'Remove from group': '移出群聊',
'Add member': '添加成员',
-@@ -215,6 +215,25 @@ const ZH = {
+@@ -215,6 +423,25 @@
'This model is not available now. Pick another one.': '这个模型现在不可用,请换一个。',
'Every message of this Bot runs on it.': '这个 Bot 的每条消息都用它。',
'Applies from the next message.': '从下一条消息开始生效。',
@@ -784,7 +788,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Profile: '资料',
Label: '标签',
'Optional. Shown under the name.': '可选,显示在名字下方。',
-@@ -238,6 +238,7 @@ const ZH = {
+@@ -238,6 +465,7 @@
'Kept above the other chats.': '排在其他对话上面。',
'Search still finds it.': '仍然可以搜索到。',
Manage: '管理',
@@ -792,7 +796,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Group chat settings
'Leave it empty to name the group after its members.': '留空就按成员命名。',
-@@ -274,6 +274,8 @@ const ZH = {
+@@ -274,6 +502,8 @@
'Harness default': 'Harness 默认',
Saving: '保存中',
'Manage models': '管理模型',
@@ -801,7 +805,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Shapes
Whale: '鲸鱼', Star: '星星', Moon: '月亮', Wave: '声波', Squircle: '圆角方', Bull: '牛角', Blossom: '花朵',
-@@ -312,9 +312,10 @@ const ZH = {
+@@ -312,9 +542,10 @@
'No group chats yet.': '还没有群聊。',
'A group chat lets several Bots work on one thing with you.': '在群聊里,几个 Bot 和你一起做一件事。',
'Back to group chats': '返回群聊列表',
@@ -814,7 +818,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Harness settings': 'Harness 设置',
'Back to Bots': '返回 Bot 列表',
'Close settings': '关闭设置',
-@@ -324,7 +324,6 @@ const ZH = {
+@@ -324,7 +555,6 @@
Pin: '置顶',
Pinned: '已置顶',
'Mark as Unread': '标为未读',
@@ -822,7 +826,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Group settings': '群设置',
'Rename group': '重命名群聊',
'Copy conversation ID': '复制对话 ID',
-@@ -333,6 +333,8 @@ const ZH = {
+@@ -333,6 +563,8 @@
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
'Add DSH Agent': '添加 DSH Agent',
@@ -831,7 +835,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Remove DSH Agent': '移除 DSH Agent',
'New session': '新会话',
Archive: '归档',
-@@ -344,13 +344,13 @@ const ZH = {
+@@ -344,13 +576,13 @@
'{n} d': '{n} 天',
// Plugin updates
@@ -851,7 +855,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'You have {version}.': '当前版本 {version}。',
'The update did not finish: {reason}': '更新没有完成:{reason}',
"What's new": '更新说明',
-@@ -360,9 +360,62 @@ const ZH = {
+@@ -360,9 +592,62 @@
'DSH update': 'DSH 更新',
'DSH {version}': 'DSH {version}',
'Restart DSH to start using DSH {version}.': '重启 DSH 后开始使用 DSH {version}。',
@@ -915,7 +919,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Command palette
Messages: '消息',
Commands: '命令',
-@@ -390,6 +390,7 @@ const ZH = {
+@@ -390,6 +675,7 @@
Reply: '回复',
'More message actions': '更多操作',
More: '更多',
@@ -923,7 +927,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Reactions: '表情',
'Message actions': '消息操作',
Download: '下载',
-@@ -403,6 +403,9 @@ const ZH = {
+@@ -403,6 +689,9 @@
'Replies from': '群聊回复:',
'Reply from': '收到回复:',
'Message from': '收到消息:',
@@ -933,7 +937,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Stopped by': '叫停者:',
'{count} messages with': '{count} 条消息,来自',
'Routine started': '例行任务开始',
-@@ -412,6 +412,8 @@ const ZH = {
+@@ -412,6 +701,8 @@
'Working out a plan': '理一下思路',
'Sorting out the details': '梳理细节',
'On it': '在做了',
@@ -942,7 +946,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Busy with it': '正在处理',
'Moving ahead': '推进中',
'Taking care of it': '处理中',
-@@ -439,7 +439,6 @@ const ZH = {
+@@ -439,7 +730,6 @@
'Looking back through the chat': '翻看聊天记录',
'{name} looked up its own chat': '{name} 查看了自己的对话',
'{name} looked up its own chat for “{text}”': '{name} 在自己的对话里查找「{text}」',
@@ -950,7 +954,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
"{name} didn't answer in time": '{name} 没有及时回应',
'{name} paused for your approval': '{name} 停下来等你批准',
'{name} started on it': '{name} 开始做了',
-@@ -450,12 +450,30 @@ const ZH = {
+@@ -450,12 +740,30 @@
'Asked to make the tool call it wrote as text': '工具调用被写成了文字,已让它重新调用',
'A Bot': '一个 Bot',
'{name} is waiting for your approval': '{name} 等你批准',
@@ -982,7 +986,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
"Couldn't message {name}": '没能给 {name} 发消息',
Messaged: '已发消息给',
"Couldn't stop {name}": '没能叫停 {name}',
-@@ -485,7 +485,7 @@ const ZH = {
+@@ -485,7 +793,7 @@
'Your answer': '你的回答',
// Secrets
@@ -991,7 +995,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'No Bot': '没有 Bot',
'Paste or type the key': '粘贴或输入密钥',
'Only {name}': '只给 {name}',
-@@ -528,7 +528,7 @@ const ZH = {
+@@ -528,7 +836,7 @@
'NAME, e.g. GITHUB_TOKEN': '名字,例如 GITHUB_TOKEN',
'What it is for': '用途',
Value: '值',
@@ -1000,7 +1004,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Save key': '保存密钥',
'Keys your Bots asked for. Bots never see the values: they use them as ': '你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ',
' in their shell, and every value is replaced by ': ' 使用,而且发给模型之前,每个值都会被替换成 ',
-@@ -697,11 +697,11 @@ const ZH = {
+@@ -697,11 +1005,11 @@
'Compared with the {count} days before': '与再往前 {count} 天相比',
'{share}% of all usage from {span}': '占 {span} 全部用量的 {share}%',
// About
@@ -1015,7 +1019,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Updates: '更新',
'Check for updates': '检查更新',
'Checking…': '正在检查…',
-@@ -711,6 +711,13 @@ const ZH = {
+@@ -711,6 +1019,13 @@
'Update checks are off.': '已关闭更新检查。',
'Installed from a local folder; update it there.': '从本地文件夹安装,请在那里更新。',
'Updates are not offered for this install.': '这种安装方式不提供更新。',
@@ -1029,7 +1033,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Links: '链接',
'Source code': '源代码',
'Release notes': '发布说明',
-@@ -726,7 +726,7 @@ const ZH = {
+@@ -726,7 +1041,7 @@
Show: '查看',
Hide: '收起',
'GitHub issue': 'GitHub issue',
@@ -1038,7 +1042,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Report on GitHub': '在 GitHub 上报告',
'Copied the diagnostics.': '已复制诊断信息。',
'Could not copy. Select the text below and copy it.': '无法复制。请选中下面的文字手动复制。',
-@@ -738,7 +738,7 @@ const ZH = {
+@@ -738,7 +1053,7 @@
'Recent errors': '最近的错误',
'Recent errors · {count}': '最近的错误 · {count}',
'No errors recorded.': '没有错误记录。',
@@ -1047,7 +1051,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Model call': '模型调用',
'This window': '这个窗口',
'Error log': '错误日志',
-@@ -771,6 +771,14 @@ const ZH = {
+@@ -771,6 +1086,14 @@
'Add at least one more Bot': '再至少添加一个 Bot',
Members: '成员',
// Connectors and scheduled tasks
@@ -1062,7 +1066,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Scheduled tasks': '定时任务',
'Every day': '每天',
'Every {count} days': '每 {count} 天',
-@@ -784,7 +784,15 @@ const ZH = {
+@@ -784,7 +1107,15 @@
'Every weekend at {time}': '每个周末 {time}',
'On the schedule {expression}': '按计划 {expression}',
'Next: {time}': '下次 {time}',
@@ -1078,7 +1082,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'in {name}': '在 {name}',
'Click again to delete': '再点一次删除',
'Delete task': '删除任务',
-@@ -806,8 +806,8 @@ const ZH = {
+@@ -806,8 +1137,8 @@
Unit: '单位',
'Create task': '创建任务',
'Turn on Automation tasks in DSH': '在 DSH 里打开「自动化任务」',
@@ -1089,7 +1093,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'New task': '新建任务',
'No scheduled tasks yet.': '还没有定时任务。',
'No scheduled tasks yet. Add one': '还没有定时任务,去添加',
-@@ -823,6 +823,12 @@ const ZH = {
+@@ -823,6 +1154,12 @@
'Repeat every 1 minute to 7 days': '重复间隔要在 1 分钟到 7 天之间',
'Pick when it runs': '选择什么时候运行',
'No such task': '没有这个任务',
@@ -1102,7 +1106,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Working folder': '工作文件夹',
'Working folder: {folder}': '工作文件夹:{folder}',
Folder: '文件夹',
-@@ -855,6 +855,8 @@ const HOST_ERRORS = [
+@@ -855,6 +1192,8 @@
[/^A key can have at most (\d+) characters$/, '密钥最多 $1 个字符'],
[/^(.+) is not saved yet$/, '$1 还没有保存'],
[/^The Bot team in (.+) is open in another dsh \((.+)\)\. This window shows the team but cannot change it; close the other one and reload\.$/, '$1 里的 Bot 团队已在另一个 dsh 中打开($2)。这个窗口只能查看,不能修改;关掉另一个后刷新。'],
@@ -1111,7 +1115,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
[/^HTTP (\d+)$/, '请求失败(HTTP $1)'],
[/^GitHub answered (\d+)$/, 'GitHub 返回了 $1'],
[/^Keep the name under (\d+) characters$/, '名字不能超过 $1 个字'],
-@@ -883,6 +883,20 @@ const HOST_ZH = {
+@@ -883,6 +1222,20 @@
'Unknown Bot': '找不到这个 Bot',
'Unknown conversation': '找不到这个对话',
'A group chat needs at least two Bots': '群聊至少要两个 Bot',
@@ -1132,7 +1136,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,8 @@ const HOST_ZH = {
+@@ -905,6 +1258,8 @@
'Write the entry to save.': '请写下要保存的内容。',
'This DeepSeek Harness has no folder chooser': '这个 DeepSeek Harness 没有文件夹选择窗口',
'This DeepSeek Harness cannot list folders': '这个 DeepSeek Harness 不能列出文件夹',
@@ -4360,7 +4364,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
// A failed turn's `LlmFailure`, kept to what can be shown and logged. Like DSH's own
// error row, an AUTH failure loses its message: providers may echo part of the key.
-@@ -24,7 +24,7 @@ export const failureLine = failure => `${failure.code}${failure.status ? ` ${fai
+@@ -24,7 +47,7 @@
// A group's own Session never reaches a model, so the snapshot DSH prepared there
// rides with the user's queued message to the members who answer it.
export function attachReference(queued, source, text) {
@@ -4369,7 +4373,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
if (post === undefined || text === '') return
const labels = (Array.isArray(source?.references) ? source.references : [])
.map(reference => reference?.label).filter(label => typeof label === 'string' && label !== '')
-@@ -48,12 +48,20 @@ const RETRY_TEXT = 'Your last reply wrote a tool call as plain text (
+@@ -48,12 +71,20 @@
export function install(rt) {
const { ctx, state } = rt
@@ -4391,7 +4395,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
...(keeps && info.senderSessionId ? { fromId: info.senderSessionId } : {}),
toId: rt.selfOf(id),
sessionId: id,
-@@ -70,13 +70,13 @@ export function install(rt) {
+@@ -70,13 +101,13 @@
})
}
@@ -4407,7 +4411,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
const roomInbox = new Map()
const queueRoomMessage = (roomId, item) => roomInbox.set(roomId, [...(roomInbox.get(roomId) ?? []), item])
// Member turns of a group round the user stopped: what they end with stays out of the group.
-@@ -162,17 +162,31 @@ export function install(rt) {
+@@ -162,17 +193,31 @@
// Roles whose turn owes the sender a reply: a 'stopped' notice asks the Bot to
// acknowledge, exactly like a request.
const expectsReply = info => info.role === 'request' || info.role === 'stopped'
@@ -4440,7 +4444,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
markExchangeTurn(sessionId, entry.turn)
}
}
-@@ -196,7 +196,33 @@ export function install(rt) {
+@@ -196,7 +241,33 @@
return merged
}
@@ -4475,7 +4479,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
const workView = () => {
const bots = {}
for (const [id, entry] of live) {
-@@ -204,7 +204,9 @@ export function install(rt) {
+@@ -204,7 +275,9 @@
const owner = rt.groupOwners.get(id)
const botId = owner?.botId ?? rt.chatOwners.get(id)
if (botId === undefined || (owner !== undefined && bots[botId] !== undefined)) continue
@@ -4486,7 +4490,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
}
return bots
}
-@@ -221,6 +221,7 @@ export function install(rt) {
+@@ -221,6 +294,7 @@
botId: owner.botId,
since: entry.since,
...(entry.tool ? { tool: entry.tool } : {}),
@@ -4494,7 +4498,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
...(speaking.has(`${owner.roomId}:${owner.botId}`) ? { speaking: true } : {}),
}]
}
-@@ -238,6 +238,45 @@ export function install(rt) {
+@@ -238,6 +312,53 @@
return { ...(text ? { text: text.trim().slice(0, 300) } : {}), ...(code ? { code: code.trim().slice(0, 400) } : {}) }
}
@@ -4515,7 +4519,10 @@ diff --git a/src/host/turns.js b/src/host/turns.js
+ .filter(entry => entry.failed !== undefined && latest.get(placeOfEnded(entry)) === entry && now - entry.at <= FAILED_MS)
+ return [...older.slice(-FAILED_KEPT), ...list.slice(-ENDED_KEPT)]
+ }
-+ const noteEnded = (id, turn, info, reason, failure, calls) => {
++ // A turn that died before writing a line leaves the chat looking like a Bot that chose to stay
++ // quiet (#1044), so a failure with no text is marked `silent` and carries the streak of them at the
++ // same place: one blip and a provider that has been refusing for hours read differently.
++ const noteEnded = (id, turn, info, reason, failure, calls, texts = 0) => {
+ if (failure === undefined && reason?.kind !== 'completed') return
+ const owner = rt.groupOwners.get(id)
+ const bot = rt.botOf(owner?.botId ?? rt.selfOf(id))
@@ -4524,6 +4531,10 @@ diff --git a/src/host/turns.js b/src/host/turns.js
+ if (owner === undefined && state.exchangeTurns?.[id]?.includes(turn)) return
+ if (owner !== undefined && rt.roomOf(owner.roomId)?.origin !== undefined) return
+ const at = Date.now()
++ const place = `${bot.id}|${owner?.roomId ?? ''}`
++ const silent = failure !== undefined && texts === 0
++ const lastHere = ended.findLast?.(entry => placeOfEnded(entry) === place) ?? [...ended].reverse().find(entry => placeOfEnded(entry) === place)
++ const streak = silent ? (lastHere?.silent === true ? (lastHere.streak ?? 0) + 1 : 1) : 0
+ ended = trimEnded([...ended, {
+ id: `${id}:${turn}:${at}`,
+ botId: bot.id,
@@ -4531,6 +4542,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
+ at,
+ ...(calls > 0 ? { calls } : {}),
+ ...(failure ? { failed: (failure.message || failure.code).slice(0, 160) } : {}),
++ ...(silent ? { silent: true, streak } : {}),
+ }], at)
+ }
+ const endedView = () => ended
@@ -4540,7 +4552,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
// Where a Session speaks, for the diagnostics log.
const placeOf = (id) => {
const room = rt.roomOf(rt.groupOwners.get(id)?.roomId) ?? rt.roomOf(id)
-@@ -266,6 +266,7 @@ export function install(rt) {
+@@ -266,6 +387,7 @@
case 'tool/call': {
const name = String(event.data?.name ?? '')
startCall(id, entry, event.data?.callId, name, String(event.data?.arguments ?? ''))
@@ -4548,7 +4560,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
// A program is as quick as the tools it runs, so its sub-calls decide.
if (rt.groupOwners.has(id) && name !== PROGRAM_TOOL) rt.workRoomTurn?.(id, name)
break
-@@ -288,11 +288,17 @@ export function install(rt) {
+@@ -288,11 +410,17 @@
entry.since = event.time ?? Date.now()
entry.texts = []
entry.info = undefined
@@ -4566,7 +4578,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
if (entry.pending !== undefined) {
claimTrigger(id, entry, entry.pending)
entry.pending = undefined
-@@ -300,6 +300,14 @@ export function install(rt) {
+@@ -300,6 +428,14 @@
break
case 'user/message': {
const source = event.data?.source
@@ -4581,7 +4593,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
// A message that shares a step with a bare handoff package is what the turn
// answers, so the turn is not a hidden acknowledgement.
const bare = info => info?.handoff === true && info.resume !== true
-@@ -331,15 +331,27 @@ export function install(rt) {
+@@ -331,15 +467,27 @@
hop: source.hop ?? 0,
...(source.late ? { late: true } : {}),
...(source.quiet ? { quiet: true } : {}),
@@ -4611,7 +4623,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
if (entry.turn === undefined) entry.pending = source
else if (!opened) claimTrigger(id, entry, source)
else if (expectsReply(source) && !answersUser(source)
-@@ -363,12 +363,15 @@ export function install(rt) {
+@@ -363,12 +511,15 @@
if (rt.roomOf(id) && source.kind === 'user') {
const images = (Array.isArray(event.data.content) ? event.data.content : [])
.filter(block => block?.type === 'image' && !block.offloaded).map(block => block.attachment)
@@ -4632,7 +4644,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
delete state.questions[id]
void rt.save().catch(() => {})
}
-@@ -386,11 +386,17 @@ export function install(rt) {
+@@ -386,11 +537,17 @@
case 'turn/end': {
setActivity(id, undefined)
const info = entry.info
@@ -4650,7 +4662,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
entry.turn = undefined
entry.info = undefined
entry.texts = []
-@@ -399,11 +399,14 @@ export function install(rt) {
+@@ -399,11 +556,14 @@
entry.since = undefined
entry.tool = undefined
entry.calls = undefined
@@ -4665,17 +4677,17 @@ diff --git a/src/host/turns.js b/src/host/turns.js
if (switched) break
// A model that writes its tool call as text ends the turn as if it were done,
// though nothing ran. It gets one more turn on the same trigger, so a waiting
-@@ -411,7 +411,8 @@ export function install(rt) {
+@@ -411,7 +571,8 @@
// the call.
const wroteCall = failure === undefined && reason?.kind === 'completed' && lastCalled !== true && writesToolCall(lastText)
const retry = wroteCall && info?.retried !== true && !(info?.role === 'room' && stoppedRoomTurns.has(info.exchangeId))
- if (retry) void retryWrittenCall(id, info).catch(error => rt.warn('ds-bot: asking %s to run its written tool call failed: %s', id, error))
+ if (retry) void retryWrittenCall(id, info, origin).catch(error => rt.warn('ds-bot: asking %s to run its written tool call failed: %s', id, error))
-+ if (!retry) noteEnded(id, answeredTurn, info, reason, failure, toolCalls)
++ if (!retry) noteEnded(id, answeredTurn, info, reason, failure, toolCalls, texts.length)
const replyText = () => {
const reply = (wroteCall ? texts.map(stripWrittenCalls) : texts).join('\n\n').trim()
return wroteCall && reply === '' ? 'Tried to run a tool but wrote the call as text, so it did not run.' : reply
-@@ -419,6 +419,24 @@ export function install(rt) {
+@@ -419,6 +580,24 @@
// Whoever the turn answers gets the same three-way rule: a failure notice,
// the turn's text (or a stopped-before-answer line), or nothing.
const answer = (source, reply) => {
@@ -4700,7 +4712,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
if (!expectsReply(source) || answersUser(source)) return
const name = rt.botOf(rt.selfOf(id))?.name ?? 'The other Bot'
// A sender that wrote from a group gets the reply in its Session there, while
-@@ -434,6 +434,7 @@ export function install(rt) {
+@@ -434,6 +613,7 @@
role: 'reply',
hop: (source.hop ?? 0) + 1,
replyTo: source.exchangeId,
@@ -4708,7 +4720,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
}).catch(error => rt.warn('ds-bot: failure notice delivery failed: %s', error))
} else if (reply !== '' || reason?.kind === 'aborted') {
// A turn stopped before it could answer still owes the sender a line,
-@@ -446,6 +446,7 @@ export function install(rt) {
+@@ -446,6 +626,7 @@
role: 'reply',
hop: (source.hop ?? 0) + 1,
replyTo: source.exchangeId,
@@ -4716,7 +4728,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
}).catch(error => rt.warn('ds-bot: reply delivery failed: %s', error))
}
}
-@@ -460,14 +460,14 @@ export function install(rt) {
+@@ -460,14 +641,14 @@
} else if (!stopped && reason?.kind !== 'aborted') {
// The round moved on without this member; how its turn ends still reaches the group.
if (failure !== undefined) void rt.lateRoomFailure?.(id, info, failure)
@@ -4733,7 +4745,7 @@ diff --git a/src/host/turns.js b/src/host/turns.js
} else {
answer(info, replyText())
}
-@@ -488,13 +488,27 @@ export function install(rt) {
+@@ -488,13 +669,27 @@
hop: request.info.hop ?? 0,
exchangeId: request.info.exchangeId,
...(request.info.senderRoomId ? { fromRoomId: request.info.senderRoomId } : {}),
@@ -5087,7 +5099,7 @@ diff --git a/src/shared/mentions.js b/src/shared/mentions.js
diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
--- a/test/approval-notices.test.js
+++ b/test/approval-notices.test.js
-@@ -3,6 +3,8 @@ import { test } from 'node:test'
+@@ -3,6 +3,8 @@
import { fileURLToPath } from 'node:url'
import { runInNewContext } from 'node:vm'
import { build } from 'esbuild'
@@ -5096,7 +5108,7 @@ diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
const bundle = await build({
entryPoints: [fileURLToPath(new URL('../src/client/approval-notices.js', import.meta.url))],
-@@ -10,7 +10,7 @@ const bundle = await build({
+@@ -10,7 +12,7 @@
})
// One window: the platform's Notification, whether the page shows, and the work source.
@@ -5105,7 +5117,7 @@ diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
const shown = []
const opened = []
function Notification(title, options) {
-@@ -26,14 +26,16 @@ function windowWith({ permission = 'granted', visibility = 'visible' } = {}) {
+@@ -26,14 +28,16 @@
subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener) },
set: (next) => { snapshot = next; for (const listener of listeners) listener() },
}
@@ -5124,7 +5136,7 @@ diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
}
test('an approval that starts waiting while another conversation shows gets one notification', () => {
-@@ -41,7 +41,7 @@ test('an approval that starts waiting while another conversation shows gets one
+@@ -41,7 +45,7 @@
page.noticeApprovals({ work: page.work, roster: page.roster, shownSession: () => 'a', openSession: page.openSession, since: 100 })
page.work.set({ bots: {}, approvals: [{ id: 'x1', botId: 'b', roomId: 'r1', sessionId: 'gb', tool: 'pwsh', at: 200 }] })
page.work.set({ bots: {}, approvals: [{ id: 'x1', botId: 'b', roomId: 'r1', sessionId: 'gb', tool: 'pwsh', at: 200 }] })
@@ -5133,7 +5145,7 @@ diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
page.shown[0].onclick()
assert.deepEqual([page.opened, page.shown[0].closed], [['r1'], true])
})
-@@ -62,3 +62,236 @@ test('no notification for the conversation on screen, for approvals older than t
+@@ -62,3 +66,236 @@
denied.work.set({ bots: {}, approvals: [{ id: 'x1', botId: 'b', roomId: 'r1', tool: 'pwsh', at: 200 }] })
assert.equal(denied.shown.length, 0)
})
@@ -5322,8 +5334,8 @@ diff --git a/test/approval-notices.test.js b/test/approval-notices.test.js
+ turn('g2', 1, completed, { kind: SOURCE_KIND, role: 'room', exchangeId: 'r2x' })
+ assert.deepEqual(rt.endedView().map(({ id, at, ...entry }) => [id.split(':').slice(0, 2).join(':'), entry]), [
+ ['b:1', { botId: 'b', sessionId: 'b' }],
-+ ['b:2', { botId: 'b', sessionId: 'b', failed: 'upstream busy' }],
-+ ['g1:1', { botId: 'b', roomId: 'r1', failed: 'upstream busy' }],
++ ['b:2', { botId: 'b', sessionId: 'b', failed: 'upstream busy', silent: true, streak: 1 }],
++ ['g1:1', { botId: 'b', roomId: 'r1', failed: 'upstream busy', silent: true, streak: 1 }],
+ ])
+ for (let n = 10; n < 40; n++) turn('b', n, completed)
+ // Past the last 20, the failure in r1 stays: it is still the latest turn there (#471).
diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch
index 2de24c7f9..0f5b4de10 100644
--- a/patches/ds-bot/0003-flybot-api-client.patch
+++ b/patches/ds-bot/0003-flybot-api-client.patch
@@ -1214,8 +1214,11 @@ diff --git a/src/client/icons.js b/src/client/icons.js
diff --git a/src/client/index.js b/src/client/index.js
--- a/src/client/index.js
+++ b/src/client/index.js
-@@ -1,44 +1,65 @@
+@@ -1,44 +1,68 @@
+import { FLYBOT_CSS, FlyBotApprovals, FlyBotSessionDock, createClientApi as createFlyBotClient } from './flybot-slots.js'
++import { createElement as h } from 'react'
++import { registry as flybotSlots } from './flybot-registry.js'
++import { TURN_FAILURES_CSS, TurnFailures } from './turn-failures.js'
import { call } from './api.js'
-import { isHex, inkText, paintCss } from './inks.js'
-import { DEFAULT_MASK, SHAPES, markupCache, maskOf } from './characters.js'
@@ -1289,7 +1292,7 @@ diff --git a/src/client/index.js b/src/client/index.js
migrateStorage()
const UNREAD_KEY = STORAGE.unread
let savedUnread = {}
-@@ -50,9 +50,12 @@ export function apply(ctx) {
+@@ -50,9 +74,12 @@
const SEEN_KEY = STORAGE.seen
let seenAt = {}
try { seenAt = JSON.parse(localStorage.getItem(SEEN_KEY) ?? '{}') ?? {} } catch { seenAt = {} }
@@ -1303,7 +1306,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// Bot mode paints the whole team surface; Agent mode withdraws every slot and
// style so the shell's own interface returns. `plain` stays false until a Bot-mode
// view hosts a plain session, which drops only the conversation replacements.
-@@ -125,6 +125,17 @@ export function apply(ctx) {
+@@ -125,6 +152,17 @@
try { localStorage.setItem(UNREAD_KEY, JSON.stringify(unread)) } catch { /* storage unavailable */ }
return { ...state, unread }
})
@@ -1321,7 +1324,7 @@ diff --git a/src/client/index.js b/src/client/index.js
const refreshed = new Set()
// Pulls can overlap (the poll and a navigation); an older answer never wins.
let pulls = 0
-@@ -132,7 +132,8 @@ export function apply(ctx) {
+@@ -132,7 +170,8 @@
const pull = async () => {
const order = ++pulls
const known = roster.getSnapshot()
@@ -1331,7 +1334,7 @@ diff --git a/src/client/index.js b/src/client/index.js
if (order < applied) return
applied = order
const nextActivity = JSON.stringify(value.activity ?? {})
-@@ -145,11 +145,14 @@ export function apply(ctx) {
+@@ -145,11 +184,14 @@
const live = {}
if (value.connectors !== undefined && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live.connectors = value.connectors
if (value.capabilityStatuses !== undefined && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live.capabilityStatuses = value.capabilityStatuses
@@ -1346,7 +1349,7 @@ diff --git a/src/client/index.js b/src/client/index.js
try {
const ids = (value.agents ?? []).flatMap(agent => agent.sessions ?? [])
localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids))
-@@ -252,17 +252,7 @@ export function apply(ctx) {
+@@ -252,17 +294,7 @@
const style = installStyles('', 'mentions')
let last = ''
const render = () => {
@@ -1365,7 +1368,7 @@ diff --git a/src/client/index.js b/src/client/index.js
if (next !== last) style.textContent = last = next
}
render()
-@@ -325,7 +325,7 @@ export function apply(ctx) {
+@@ -325,7 +357,7 @@
ctx.inject(['shortcuts'], scope => { scope.effect(() => holdShellShortcuts(scope.shortcuts), 'ds-bot: settings shortcut') })
@@ -1374,7 +1377,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// A Bot id, or any part of its chat, opens the Bot's current part.
const partFor = (id) => {
const bot = roster.getSnapshot().byId[id]
-@@ -342,6 +342,7 @@ export function apply(ctx) {
+@@ -342,6 +374,7 @@
// or the click appears to do nothing.
isAgentSession: id => isAgentSessionId(id),
openSession: (id) => {
@@ -1382,7 +1385,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// A DSH Agent Session id lands on the plain surface, not the Bot skin.
if (isAgentSessionId(id)) return actions.openAgentSession(id)
ui.set(state => (state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state))
-@@ -409,11 +409,11 @@ export function apply(ctx) {
+@@ -409,11 +442,11 @@
},
// `last` is the Bot conversation the view held before it cleared: when that part
// was archived for a fresh one, the view goes on in the fresh one.
@@ -1398,7 +1401,7 @@ diff --git a/src/client/index.js b/src/client/index.js
expected = target
ctx.uiWorkspace.openSession(target)
},
-@@ -444,10 +444,22 @@ export function apply(ctx) {
+@@ -444,10 +477,22 @@
// Opening the conversation would move focus to its composer and blur the name field.
renameBot: (id) => { ui.set(state => ({ ...state, details: id, view: 'details', renaming: true, tab: null })) },
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
@@ -1423,7 +1426,7 @@ diff --git a/src/client/index.js b/src/client/index.js
openConnectors: () => { ui.set(state => ({ ...state, settings: { page: 'connectors', id: null }, menu: null, palette: null, newChat: null })) },
// Agent mode hands the surface back to the shell; everything open on the Bot
// surface closes first so nothing stale reopens on the way back.
-@@ -455,7 +455,7 @@ export function apply(ctx) {
+@@ -455,7 +500,7 @@
if (mode !== 'bot' && mode !== 'agent') return
if (surface.getSnapshot().mode === mode) return
if (mode === 'agent') {
@@ -1432,7 +1435,7 @@ diff --git a/src/client/index.js b/src/client/index.js
}
writeMode(mode)
surface.set(state => ({ ...state, mode }))
-@@ -487,6 +487,12 @@ export function apply(ctx) {
+@@ -487,6 +532,12 @@
const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null)
ui.set(state => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } }))
},
@@ -1445,7 +1448,7 @@ diff --git a/src/client/index.js b/src/client/index.js
endVisit: (id) => {
leaveVisits([id])
ui.set((state) => {
-@@ -532,6 +532,7 @@ export function apply(ctx) {
+@@ -532,6 +583,7 @@
answerQuestion: (sessionId, questionId, answer) => after(call('answer-question', { sessionId: partFor(sessionId), questionId, answer })),
dismissQuestion: sessionId => after(call('dismiss-question', { sessionId })),
answerApproval: (id, choice) => after(call('approval-answer', { id, choice })),
@@ -1453,7 +1456,7 @@ diff --git a/src/client/index.js b/src/client/index.js
stopMemberWork: (roomId, botId) => after(call('stop-member-work', { roomId, botId })),
// Values go out in these requests and never come back in a response.
secretSet: payload => after(call('secret-set', payload)),
-@@ -539,15 +539,26 @@ export function apply(ctx) {
+@@ -539,15 +591,26 @@
secretCancel: requestId => after(call('secret-cancel', { requestId })),
secretUpdate: (name, scope) => after(call('secret-update', { name, scope })),
secretDelete: name => after(call('secret-delete', { name })),
@@ -1481,7 +1484,7 @@ diff --git a/src/client/index.js b/src/client/index.js
scheduleDelete: (sessionId, id) => call('schedule-delete', { sessionId, id }),
exchange: (a, b) => call('exchange', { a, b }).catch(() => []),
roomProgress: roomId => call('room-progress', { roomId }).catch(() => []),
-@@ -566,6 +566,8 @@ export function apply(ctx) {
+@@ -566,6 +629,8 @@
noteClientError(where, error)
ui.set(state => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }))
},
@@ -1490,7 +1493,7 @@ diff --git a/src/client/index.js b/src/client/index.js
dismissToast: token => ui.set(state => (state.toast && (token === undefined || state.toast.token === token) ? { ...state, toast: null } : state)),
}
// Without the Desktop bridge these stay undefined, and no browser control renders.
-@@ -610,6 +610,7 @@ export function apply(ctx) {
+@@ -610,6 +675,7 @@
}
actions.closePanel = () => { ui.set(state => (state.details ? { ...state, details: null, view: 'details', renaming: false, tab: null } : state)) }
// Whether AccountLauncher holds the shell's settings seat; FooterAccount steps aside if so.
@@ -1498,7 +1501,7 @@ diff --git a/src/client/index.js b/src/client/index.js
actions.signInEntry = () => ctx.slots.entries?.('settings.models.sign-in')?.[0] ?? null
actions.canSignIn = () => Boolean(actions.signInEntry())
actions.signIn = () => ui.set(state => ({ ...state, signIn: (state.signIn ?? 0) + 1 }))
-@@ -690,6 +690,17 @@ export function apply(ctx) {
+@@ -690,6 +756,17 @@
const browserPages = browsers?.pages ?? createSource({})
const browserTabs = browsers?.tabs ?? createSource({})
const face = () => ({ actions, hooks: { roster, ui, activity, work, memoryNews, registry, turnClock, live, cards, browserPages, browserTabs, surface, update } })
@@ -1516,7 +1519,7 @@ diff --git a/src/client/index.js b/src/client/index.js
ctx.effect(() => noticeApprovals({
work,
roster,
-@@ -715,10 +715,23 @@ export function apply(ctx) {
+@@ -715,10 +792,23 @@
}
const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component))
@@ -1540,7 +1543,7 @@ diff --git a/src/client/index.js b/src/client/index.js
const installAtMenu = () => {
const fiber = ctx.inject(['inputTriggers'], (scope) => {
scope.effect(() => {
-@@ -730,7 +730,12 @@ export function apply(ctx) {
+@@ -730,7 +820,12 @@
const launched = sessionId => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE
const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map(source => triggers.registerSource(source))
atMenu = { controllerOf }
@@ -1553,7 +1556,7 @@ diff --git a/src/client/index.js b/src/client/index.js
atMenu = null
for (const off of offs) off()
}
-@@ -739,6 +739,14 @@ export function apply(ctx) {
+@@ -739,6 +834,14 @@
return () => fiber.dispose()
}
actions.canReferenceChat = () => atMenu !== null
@@ -1568,7 +1571,7 @@ diff --git a/src/client/index.js b/src/client/index.js
actions.referenceChat = (sessionId, span) => {
const controller = atMenu?.controllerOf(sessionId)
if (controller === undefined || span === undefined) return
-@@ -752,33 +752,46 @@ export function apply(ctx) {
+@@ -752,33 +855,46 @@
installCompiledStyles(SHELL_CSS, 'shell'),
installCompiledStyles(SIDEBAR_CSS, 'sidebar'),
installCompiledStyles(SECRET_CSS, 'secrets'),
@@ -1618,7 +1621,7 @@ diff --git a/src/client/index.js b/src/client/index.js
shadow('conversation.chat.node', { key: 'assistant-step', priority: -10, locale: 'chat', inject: assistantFace }, AssistantCell),
shadow('conversation.chat.node', { key: 'turn-trigger', priority: -10, locale: 'chat', inject: face }, TriggerCell),
shadow('conversation.chat.node', { key: 'tool-call', priority: -10, locale: 'chat', inject: face }, ToolCell),
-@@ -790,15 +790,24 @@ export function apply(ctx) {
+@@ -790,15 +906,24 @@
// every message.
shadow('conversation.chat.node', { key: 'question-reply', priority: -10, locale: 'chat' }, AnsweredCell),
shadow('conversation.chat.node', { key: 'compaction', priority: -10, locale: 'chat' }, Nothing),
@@ -1646,14 +1649,21 @@ diff --git a/src/client/index.js b/src/client/index.js
shadow('conversation.input.right', { id: 'bot-voice', order: 100, inject: face }, ComposerVoice),
shadow('conversation.composer', {
priority: -10,
-@@ -808,6 +808,7 @@ export function apply(ctx) {
+@@ -808,7 +933,14 @@
shadow('conversation.input.dock', { id: 'bot-model', order: -110, inject: face }, ModelChoiceDock),
shadow('conversation.input.dock', { id: 'bot-question', order: -100, inject: face }, QuestionDock),
shadow('conversation.input.dock', { id: 'bot-secret', order: -90, inject: face }, SecretDock),
+ shadow('conversation.input.dock', { id: 'flybot-session', order: -80, inject: face }, FlyBotSessionDock),
]
++ // A turn that died before writing a word says so above the composer, in every chat (#1044).
++ ctx.effect(() => {
++ const style = installStyles(TURN_FAILURES_CSS, 'turn-failures')
++ const off = flybotSlots().register('flybot.session.dock', { id: 'flybot-turn-failures', order: 20, component: props => h(TurnFailures, { ...props, work }) })
++ return () => { off(); style.remove() }
++ }, 'ds-bot: dead turn lines')
// The return button belongs to neither group: it renders only in Agent mode.
ctx.effect(() => {
+ const style = installStyles(RETURN_CSS, 'return')
diff --git a/src/client/new-chat.js b/src/client/new-chat.js
--- a/src/client/new-chat.js
+++ b/src/client/new-chat.js
diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch
index bc000fe0e..b18472a41 100644
--- a/patches/ds-bot/0008-client-bundle.patch
+++ b/patches/ds-bot/0008-client-bundle.patch
@@ -3,7 +3,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gener
diff --git a/client.js b/client.js
--- a/client.js
+++ b/client.js
-@@ -49,6 +49,59 @@ var ZH = {
+@@ -49,6 +49,63 @@
"Unread activity": "有未读",
Search: "搜索",
"New chat": "新对话",
@@ -22,6 +22,10 @@ diff --git a/client.js b/client.js
+ "Enter a number, or paste a group invite link": "输入数字面对面建群,或粘贴邀请链接加入",
+ "Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.": "选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。",
+ "Or paste a group invite link (https://…/r/…)": "或粘贴群邀请链接(https://…/r/…)",
++ // #1044: a turn that died before writing anything still says so where the chat is.
++ "Could not run: {error}": "这一轮没能跑起来:{error}",
++ "the model call failed": "模型调用失败",
++ "{count} turns in a row": "连续 {count} 轮",
+ "Group invite link": "群邀请链接",
+ "Room-only: friends do not see these Bots unless you invite them.": "仅限这个房间:除非你邀请,好友看不到这些 Bot。",
+ "6 digits": "6 位数字",
@@ -63,7 +67,7 @@ diff --git a/client.js b/client.js
"New…": "新建",
"Animation": "动画",
"Bot animation": "Bot 动画",
-@@ -63,6 +63,21 @@ var ZH = {
+@@ -63,6 +120,21 @@
Duplicate: "复制一份",
Hidden: "已隐藏",
Admin: "管理员",
@@ -85,7 +89,7 @@ diff --git a/client.js b/client.js
Add: "添加",
Close: "关闭",
Copy: "复制",
-@@ -78,21 +78,68 @@ var ZH = {
+@@ -78,21 +150,68 @@
" and ": "、",
and: "和",
"{count} members": "{count} 位成员",
@@ -157,7 +161,7 @@ diff --git a/client.js b/client.js
Connectors: "连接器",
"Search connectors": "搜索连接器",
"No connectors": "暂无连接器",
-@@ -129,6 +129,59 @@ var ZH = {
+@@ -129,6 +248,59 @@
"Connecting…": "正在连接…",
"Could not connect: {error}": "连接失败:{error}",
"Connected as {account}": "已连接:{account}",
@@ -217,7 +221,7 @@ diff --git a/client.js b/client.js
Connected: "已连接",
Disconnect: "断开",
Connect: "连接",
-@@ -141,17 +141,44 @@ var ZH = {
+@@ -141,17 +313,44 @@
"GitHub addresses must start with https://": "GitHub 地址必须以 https:// 开头",
"This DSH cannot load connectors": "这个 DSH 不能加载连接器",
"No such connector": "没有这个连接器",
@@ -266,7 +270,7 @@ diff --git a/client.js b/client.js
// Header
"Message {name}": "给 {name} 发消息",
Message: "发消息",
-@@ -162,8 +162,6 @@ var ZH = {
+@@ -162,8 +361,6 @@
"Dismiss new messages": "关闭新消息提示",
New: "新",
// New chat
@@ -275,7 +279,7 @@ diff --git a/client.js b/client.js
"Add to group chat": "加入群聊",
"Message Bot": "给 Bot 发消息",
"To:": "发给:",
-@@ -187,9 +187,20 @@ var ZH = {
+@@ -187,9 +384,20 @@
Everyone: "所有人",
"Every member answers in turn, admin first.": "每位成员轮流回答,管理员先说。",
"Members · {count}": "成员 · {count}",
@@ -296,7 +300,7 @@ diff --git a/client.js b/client.js
"Remove {name} from the group": "把 {name} 移出群聊",
"Remove from group": "移出群聊",
"Add member": "添加成员",
-@@ -247,6 +247,25 @@ var ZH = {
+@@ -247,6 +455,25 @@
"This model is not available now. Pick another one.": "这个模型现在不可用,请换一个。",
"Every message of this Bot runs on it.": "这个 Bot 的每条消息都用它。",
"Applies from the next message.": "从下一条消息开始生效。",
@@ -322,7 +326,7 @@ diff --git a/client.js b/client.js
Profile: "资料",
Label: "标签",
"Optional. Shown under the name.": "可选,显示在名字下方。",
-@@ -270,6 +270,7 @@ var ZH = {
+@@ -270,6 +497,7 @@
"Kept above the other chats.": "排在其他对话上面。",
"Search still finds it.": "仍然可以搜索到。",
Manage: "管理",
@@ -330,7 +334,7 @@ diff --git a/client.js b/client.js
// Group chat settings
"Leave it empty to name the group after its members.": "留空就按成员命名。",
"Every member reads it before speaking in this group.": "每位成员在这个群里发言前都会读到。",
-@@ -303,6 +303,8 @@ var ZH = {
+@@ -303,6 +531,8 @@
"Harness default": "Harness 默认",
Saving: "保存中",
"Manage models": "管理模型",
@@ -339,7 +343,7 @@ diff --git a/client.js b/client.js
// Shapes
Whale: "鲸鱼",
Star: "星星",
-@@ -374,9 +374,10 @@ var ZH = {
+@@ -374,9 +604,10 @@
"No group chats yet.": "还没有群聊。",
"A group chat lets several Bots work on one thing with you.": "在群聊里,几个 Bot 和你一起做一件事。",
"Back to group chats": "返回群聊列表",
@@ -352,7 +356,7 @@ diff --git a/client.js b/client.js
"Harness settings": "Harness 设置",
"Back to Bots": "返回 Bot 列表",
"Close settings": "关闭设置",
-@@ -385,7 +385,6 @@ var ZH = {
+@@ -385,7 +616,6 @@
Pin: "置顶",
Pinned: "已置顶",
"Mark as Unread": "标为未读",
@@ -360,7 +364,7 @@ diff --git a/client.js b/client.js
"Group settings": "群设置",
"Rename group": "重命名群聊",
"Copy conversation ID": "复制对话 ID",
-@@ -394,6 +394,8 @@ var ZH = {
+@@ -394,6 +624,8 @@
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
"Add DSH Agent": "添加 DSH Agent",
@@ -369,7 +373,7 @@ diff --git a/client.js b/client.js
"Remove DSH Agent": "移除 DSH Agent",
"New session": "新会话",
Archive: "归档",
-@@ -404,13 +404,13 @@ var ZH = {
+@@ -404,13 +636,13 @@
"{n} h": "{n} 小时",
"{n} d": "{n} 天",
// Plugin updates
@@ -389,7 +393,7 @@ diff --git a/client.js b/client.js
"You have {version}.": "当前版本 {version}。",
"The update did not finish: {reason}": "更新没有完成:{reason}",
"What's new": "更新说明",
-@@ -420,8 +420,61 @@ var ZH = {
+@@ -420,8 +652,61 @@
"DSH update": "DSH 更新",
"DSH {version}": "DSH {version}",
"Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。",
@@ -452,7 +456,7 @@ diff --git a/client.js b/client.js
// Command palette
Messages: "消息",
Commands: "命令",
-@@ -446,6 +446,7 @@ var ZH = {
+@@ -446,6 +731,7 @@
Reply: "回复",
"More message actions": "更多操作",
More: "更多",
@@ -460,7 +464,7 @@ diff --git a/client.js b/client.js
Reactions: "表情",
"Message actions": "消息操作",
Download: "下载",
-@@ -459,6 +459,9 @@ var ZH = {
+@@ -459,6 +745,9 @@
"Replies from": "群聊回复:",
"Reply from": "收到回复:",
"Message from": "收到消息:",
@@ -470,7 +474,7 @@ diff --git a/client.js b/client.js
"Stopped by": "叫停者:",
"{count} messages with": "{count} 条消息,来自",
"Routine started": "例行任务开始",
-@@ -468,6 +468,8 @@ var ZH = {
+@@ -468,6 +757,8 @@
"Working out a plan": "理一下思路",
"Sorting out the details": "梳理细节",
"On it": "在做了",
@@ -479,7 +483,7 @@ diff --git a/client.js b/client.js
"Busy with it": "正在处理",
"Moving ahead": "推进中",
"Taking care of it": "处理中",
-@@ -495,7 +495,6 @@ var ZH = {
+@@ -495,7 +786,6 @@
"Looking back through the chat": "翻看聊天记录",
"{name} looked up its own chat": "{name} 查看了自己的对话",
"{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」",
@@ -487,7 +491,7 @@ diff --git a/client.js b/client.js
"{name} didn't answer in time": "{name} 没有及时回应",
"{name} paused for your approval": "{name} 停下来等你批准",
"{name} started on it": "{name} 开始做了",
-@@ -506,12 +506,30 @@ var ZH = {
+@@ -506,12 +796,30 @@
"Asked to make the tool call it wrote as text": "工具调用被写成了文字,已让它重新调用",
"A Bot": "一个 Bot",
"{name} is waiting for your approval": "{name} 等你批准",
@@ -519,7 +523,7 @@ diff --git a/client.js b/client.js
"Couldn't message {name}": "没能给 {name} 发消息",
Messaged: "已发消息给",
"Couldn't stop {name}": "没能叫停 {name}",
-@@ -539,7 +539,7 @@ var ZH = {
+@@ -539,7 +847,7 @@
Dismissed: "已关闭",
"Your answer": "你的回答",
// Secrets
@@ -528,7 +532,7 @@ diff --git a/client.js b/client.js
"No Bot": "没有 Bot",
"Paste or type the key": "粘贴或输入密钥",
"Only {name}": "只给 {name}",
-@@ -582,7 +582,7 @@ var ZH = {
+@@ -582,7 +890,7 @@
"NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN",
"What it is for": "用途",
Value: "值",
@@ -537,7 +541,7 @@ diff --git a/client.js b/client.js
"Save key": "保存密钥",
"Keys your Bots asked for. Bots never see the values: they use them as ": "你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ",
" in their shell, and every value is replaced by ": " 使用,而且发给模型之前,每个值都会被替换成 ",
-@@ -747,11 +747,11 @@ var ZH = {
+@@ -747,11 +1055,11 @@
"Compared with the {count} days before": "与再往前 {count} 天相比",
"{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%",
// About
@@ -552,7 +556,7 @@ diff --git a/client.js b/client.js
Updates: "更新",
"Check for updates": "检查更新",
"Checking…": "正在检查…",
-@@ -761,6 +761,13 @@ var ZH = {
+@@ -761,6 +1069,13 @@
"Update checks are off.": "已关闭更新检查。",
"Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。",
"Updates are not offered for this install.": "这种安装方式不提供更新。",
@@ -566,7 +570,7 @@ diff --git a/client.js b/client.js
Links: "链接",
"Source code": "源代码",
"Release notes": "发布说明",
-@@ -776,7 +776,7 @@ var ZH = {
+@@ -776,7 +1091,7 @@
Show: "查看",
Hide: "收起",
"GitHub issue": "GitHub issue",
@@ -575,7 +579,7 @@ diff --git a/client.js b/client.js
"Report on GitHub": "在 GitHub 上报告",
"Copied the diagnostics.": "已复制诊断信息。",
"Could not copy. Select the text below and copy it.": "无法复制。请选中下面的文字手动复制。",
-@@ -788,7 +788,7 @@ var ZH = {
+@@ -788,7 +1103,7 @@
"Recent errors": "最近的错误",
"Recent errors · {count}": "最近的错误 · {count}",
"No errors recorded.": "没有错误记录。",
@@ -584,7 +588,7 @@ diff --git a/client.js b/client.js
"Model call": "模型调用",
"This window": "这个窗口",
"Error log": "错误日志",
-@@ -821,6 +821,14 @@ var ZH = {
+@@ -821,6 +1136,14 @@
"Add at least one more Bot": "再至少添加一个 Bot",
Members: "成员",
// Connectors and scheduled tasks
@@ -599,7 +603,7 @@ diff --git a/client.js b/client.js
"Scheduled tasks": "定时任务",
"Every day": "每天",
"Every {count} days": "每 {count} 天",
-@@ -834,7 +834,15 @@ var ZH = {
+@@ -834,7 +1157,15 @@
"Every weekend at {time}": "每个周末 {time}",
"On the schedule {expression}": "按计划 {expression}",
"Next: {time}": "下次 {time}",
@@ -615,7 +619,7 @@ diff --git a/client.js b/client.js
"in {name}": "在 {name}",
"Click again to delete": "再点一次删除",
"Delete task": "删除任务",
-@@ -856,8 +856,8 @@ var ZH = {
+@@ -856,8 +1187,8 @@
Unit: "单位",
"Create task": "创建任务",
"Turn on Automation tasks in DSH": "在 DSH 里打开「自动化任务」",
@@ -626,7 +630,7 @@ diff --git a/client.js b/client.js
"New task": "新建任务",
"No scheduled tasks yet.": "还没有定时任务。",
"No scheduled tasks yet. Add one": "还没有定时任务,去添加",
-@@ -873,6 +873,12 @@ var ZH = {
+@@ -873,6 +1204,12 @@
"Repeat every 1 minute to 7 days": "重复间隔要在 1 分钟到 7 天之间",
"Pick when it runs": "选择什么时候运行",
"No such task": "没有这个任务",
@@ -639,7 +643,7 @@ diff --git a/client.js b/client.js
"Working folder": "工作文件夹",
"Working folder: {folder}": "工作文件夹:{folder}",
Folder: "文件夹",
-@@ -902,6 +902,8 @@ var HOST_ERRORS = [
+@@ -902,6 +1239,8 @@
[/^A key can have at most (\d+) characters$/, "密钥最多 $1 个字符"],
[/^(.+) is not saved yet$/, "$1 还没有保存"],
[/^The Bot team in (.+) is open in another dsh \((.+)\)\. This window shows the team but cannot change it; close the other one and reload\.$/, "$1 里的 Bot 团队已在另一个 dsh 中打开($2)。这个窗口只能查看,不能修改;关掉另一个后刷新。"],
@@ -648,7 +652,7 @@ diff --git a/client.js b/client.js
[/^HTTP (\d+)$/, "请求失败(HTTP $1)"],
[/^GitHub answered (\d+)$/, "GitHub 返回了 $1"],
[/^Keep the name under (\d+) characters$/, "名字不能超过 $1 个字"],
-@@ -930,6 +930,20 @@ var HOST_ZH = {
+@@ -930,6 +1269,20 @@
"Unknown Bot": "找不到这个 Bot",
"Unknown conversation": "找不到这个对话",
"A group chat needs at least two Bots": "群聊至少要两个 Bot",
@@ -669,7 +673,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,9 @@ var HOST_ZH = {
+@@ -951,7 +1304,9 @@
"The model wrote nothing": "模型没有输出内容",
"Write the entry to save.": "请写下要保存的内容。",
"This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口",
@@ -680,7 +684,7 @@ diff --git a/client.js b/client.js
};
Object.assign(ZH, HOST_ZH);
var fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text;
-@@ -1016,6 +1016,14 @@ function noteClientError(where, error) {
+@@ -1016,6 +1371,14 @@
entries.push({ time: now, level: "error", text, ...code ? { code } : {} });
if (entries.length > MAX) entries.splice(0, entries.length - MAX);
}
@@ -695,7 +699,7 @@ diff --git a/client.js b/client.js
var quietly = (where) => (error) => noteClientError(where, error);
var clearClientErrors = () => {
entries.length = 0;
-@@ -1042,12 +1042,17 @@ var FAILURES = {
+@@ -1042,12 +1405,17 @@
};
function failureLabel(code, message) {
if (FAILURES[code]) return t(FAILURES[code]);
@@ -716,7 +720,7 @@ diff --git a/client.js b/client.js
function issueUrl(base, { what = "", diagnostics }) {
const text = what.trim().slice(0, WHAT_MAX);
const title = text.split("\n")[0].slice(0, 80);
-@@ -1066,10 +1066,16 @@ var REFUSALS = /* @__PURE__ */ new Set(["bot/refused", "bot/invalid", "bot/read-
+@@ -1066,10 +1434,16 @@
var call = async (endpoint, payload, signal) => {
let response;
try {
@@ -734,7 +738,7 @@ diff --git a/client.js b/client.js
body: JSON.stringify({ endpoint, payload: payload ?? null }),
signal
});
-@@ -1087,7 +1087,7 @@ var call = async (endpoint, payload, signal) => {
+@@ -1087,7 +1461,7 @@
if (!result.ok) {
const code = result.error?.code ?? "bot/error";
const message = hostText(result.error?.message ?? "Request failed");
@@ -743,7 +747,7 @@ diff --git a/client.js b/client.js
if (!REFUSALS.has(code)) {
noteClientError(endpoint, Object.assign(new Error(result.error?.message ?? "Request failed"), { code }));
markNoted(error);
-@@ -1097,6 +1097,373 @@ var call = async (endpoint, payload, signal) => {
+@@ -1097,6 +1471,373 @@
return result.value;
};
@@ -1117,7 +1121,7 @@ diff --git a/client.js b/client.js
// src/client/inks.js
var INKS = {
deepseek: ["#4D6BFE", "#6A84FF"],
-@@ -1177,15 +1177,92 @@ function defaultShape(id) {
+@@ -1177,15 +1918,92 @@
value = Math.imul(value ^ value >>> 13, 3266489909);
return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length];
}
@@ -1212,18 +1216,18 @@ diff --git a/client.js b/client.js
var SHAPES = {};
for (const [id, character] of Object.entries(CHARACTERS)) {
const layers = [
-@@ -1206,8 +1206,8 @@ function bodyOf(def, paint, flat = false) {
+@@ -1206,8 +2024,8 @@
return "";
}
}
-var eyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-+var eyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
-+var happyEyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
++var eyeMarkup = ([cx, cy, w, h45, tilt]) => ``;
++var happyEyeMarkup = ([cx, cy, w, h45, 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%";
-@@ -1242,162 +1242,16 @@ function maskOf(shape) {
+@@ -1242,162 +2060,16 @@
}
var DEFAULT_MASK = maskOf(FALLBACK_SHAPE);
@@ -1367,7 +1371,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"]) {
@@ -1381,34 +1394,15 @@ diff --git a/client.js b/client.js
- else delete customThemes[id];
- registry.set((value) => value + 1);
- };
-+// 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 allThemes = () => ({ ...THEMES, ...customThemes });
// src/shared/mentions.js
var ALL_WORDS = ["all", "everyone", "所有人", "全体"];
-@@ -1445,16 +1445,38 @@ function mentionSpans(text, bots, { references = [] } = {}) {
+@@ -1445,14 +2117,36 @@
}
return spans;
}
--
--// src/client/text.js
--var MENTION_ORIGIN = "https://bots.local/";
--function linkMentions(text, bots, selfId) {
-- if (!text || !text.includes("@")) return text;
-- let out = "";
-- let cursor = 0;
-- for (const span of mentionSpans(text, bots)) {
-- if (!span.bot || span.bot.id === selfId) continue;
-- out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`;
+function unmatchedMentions(text, bots, { references = [] } = {}) {
+ const value = String(text ?? "");
+ const skip = [
@@ -1430,21 +1424,20 @@ diff --git a/client.js b/client.js
+ }
+ return missing;
+}
-+
-+// src/client/text.js
-+var MENTION_ORIGIN = "https://bots.local/";
-+function linkMentions(text, bots, selfId) {
-+ if (!text || !text.includes("@")) return text;
+
+ // src/client/text.js
+ var MENTION_ORIGIN = "https://bots.local/";
+ function linkMentions(text, bots, selfId) {
+ if (!text || !text.includes("@")) return text;
+ const ordered = [...bots ?? []].sort((a, b) => Number(Boolean(a?.remote)) - Number(Boolean(b?.remote)));
-+ let out = "";
-+ let cursor = 0;
+ let out = "";
+ let cursor = 0;
+- for (const span of mentionSpans(text, bots)) {
+ for (const span of mentionSpans(text, ordered)) {
-+ if (!span.bot || span.bot.id === selfId) continue;
-+ out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`;
+ if (!span.bot || span.bot.id === selfId) continue;
+ out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`;
cursor = span.end;
- }
- return cursor === 0 ? text : out + text.slice(cursor);
-@@ -1463,6 +1463,20 @@ function mentionTarget(event) {
+@@ -1463,6 +2157,20 @@
const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null;
return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null;
}
@@ -1465,7 +1458,7 @@ diff --git a/client.js b/client.js
function clock(ms) {
return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" });
}
-@@ -1483,6 +1483,15 @@ function relativeShort(ms) {
+@@ -1483,6 +2191,15 @@
var contentText = (content) => (content ?? []).filter((block) => block?.type === "text").map((block) => block.text).join("\n");
var HEADER = /^\[(Message from|Reply from|Group chat|Group post from|Bot team setup|Handoff · part|Secret card)[^\]]*\]\n?/;
var currentPart = (bot) => bot.sessionId ?? bot.id;
@@ -1481,7 +1474,7 @@ diff --git a/client.js b/client.js
function reminderPrompts(text) {
const one = /^reminder_prompt_json: (.*)$/m.exec(text);
const batch = /^reminders_json: (.*)$/m.exec(text);
-@@ -1494,6 +1494,13 @@ function reminderPrompts(text) {
+@@ -1494,6 +2211,13 @@
}
}
var stripHeader = (text) => text.replace(HEADER, "");
@@ -1495,7 +1488,7 @@ diff --git a/client.js b/client.js
function latestMemoryChange(news, ids) {
const at = (id) => news?.[id]?.seen ? news[id].at ?? 0 : 0;
return ids.reduce((best, id) => at(id) > at(best) ? id : best, ids[0] ?? null);
-@@ -1506,25 +1506,35 @@ function summaryBlocks(text) {
+@@ -1506,25 +2230,35 @@
paragraph = [];
};
for (const raw of String(text ?? "").split("\n")) {
@@ -1535,7 +1528,7 @@ diff --git a/client.js b/client.js
var historyLoading = (snapshot) => snapshot?.openState === "cold" || snapshot?.openState === "loading";
function toolArgs(root) {
if (typeof root.args?.text === "function") {
-@@ -1597,14 +1597,21 @@ function commRunOf(order, peers, turn, hiddenTurns = []) {
+@@ -1597,14 +2331,21 @@
}
return { count, peerIds };
}
@@ -1560,65 +1553,16 @@ diff --git a/client.js b/client.js
}
return "";
}
-@@ -1624,770 +1624,1681 @@ function usageOf(list, ids) {
+@@ -1624,9 +2365,917 @@
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;
@@ -1633,112 +1577,28 @@ diff --git a/client.js b/client.js
+function awayGroups(groups, roomsById) {
+ return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face");
+}
-+
+
+-// src/client/motion.js
+// src/client/sources.js
+function createSource(initial) {
+ let value = initial;
+ const listeners2 = /* @__PURE__ */ new Set();
- return {
-- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null,
-- set(targets, curves, jump) {
-- for (const [key, to] of Object.entries(targets)) {
-- const value = values.get(key);
-- const { stiffness: k, damping: c } = curves[key];
-- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c });
-- else Object.assign(value, { to, k, c });
-- }
-- if (jump) {
-- cancelAnimationFrame(frame);
-- frame = 0;
-- apply2(read());
-- return;
-- }
-- if (!frame) {
-- last = performance.now();
-- frame = requestAnimationFrame(step);
-- }
++ return {
+ getSnapshot: () => value,
+ subscribe: (listener) => {
+ listeners2.add(listener);
+ return () => {
+ listeners2.delete(listener);
+ };
- },
-- stop: () => cancelAnimationFrame(frame)
-- };
--}
--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;
++ },
+ set(next) {
+ const resolved = typeof next === "function" ? next(value) : next;
+ if (resolved === value) return;
+ value = resolved;
+ for (const listener of [...listeners2]) listener();
- }
-- 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_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 };
- }
--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
-- );
++ }
++ };
++}
+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) {
@@ -1764,80 +1624,20 @@ diff --git a/client.js b/client.js
+ for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id;
+ }
+ return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true };
- }
--function 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 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 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 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 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 isAgentSession(roster, cache, sessionId) {
+ if (sessionId === void 0 || sessionId === null || sessionId === "") return false;
+ if (roster.ready) return roster.agentOf[sessionId] !== void 0;
@@ -1851,308 +1651,10 @@ 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}
--
--.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
--.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
--.bt-brand-whale{display:inline-flex;color:var(--bt-accent)}
--.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
--.bt-round:hover{background:var(--bt-hover)}
--.bt-round:active{transform:scale(.94)}
--.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
--.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
--.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
--.bt-tile:hover{background:var(--bt-hover)}
--.bt-tile[aria-current=page]{background:var(--bt-active)}
--.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
--.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
--.bt-row:hover{background:var(--bt-hover)}
--.bt-row[aria-current=page]{background:var(--bt-active)}
--.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
--.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
--.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
--.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
--.bt-unread-wait{background:var(--bt-star)}
--.bt-rail{align-items:center;padding:0 0 8px}
--.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
--.bt-side-foot{display:flex;padding-top:8px}
--.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
--.bt-connect:hover{background:var(--bt-hover)}
--.bt-connect svg{flex:none;width:15px;height:15px}
--.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}
--.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
--.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
--@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}}
--
--.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
--.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
--.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
--.bt-pill:active{transform:scale(.965)}
--.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
--.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
--.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
--.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
--.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)}
--.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms}
--.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)}
--.bt-pill-mem-ico svg{width:16px;height:16px}
--.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both}
--@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}}
--.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
--@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}}
--.bt-swap{color:var(--bt-ink-3);font-size:12px}
--.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease}
--.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none}
--.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)}
--.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)}
--@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}}
--
--.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
--.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
--.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
--.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
--.bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
--.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important}
--.bt-bubble p{margin:0}
--.bt-bubble p + p{margin-top:6px}
--.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px}
--.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap}
--.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat}
--.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0}
--.bt-group-msg:first-child{margin-top:0}
--.bt-lead{flex:none;display:flex;margin-bottom:1px}
--button.bt-lead{border:0;background:none;padding:0;cursor:pointer}
--/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */
--.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px}
--.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px}
--.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
--.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
--
--.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-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/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";
@@ -2371,28 +1873,7 @@ diff --git a/client.js b/client.js
+ })
+ );
+}
-
--.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0}
--.bt-dialog .bt-head{height:62px}
--.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box}
--.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px}
--.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both}
--.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none}
--@keyframes bt-sheet-in{from{transform:translateY(24px)}}
--@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}}
--.bt-exchange .bt-dialog{position:relative}
--.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin}
--.bt-exchange .bt-tools{display:none}
--.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both}
--.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none}
--.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards}
--@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}}
--@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}}
--@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}}
--.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer}
--.bt-soft:hover{background:var(--bt-active)}
--.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0}
++
+// src/client/room-invite-preview.js
+var INVITE_PREVIEW_NAME_MAX = 60;
+function stripInviteMarkers(text) {
@@ -2433,29 +1914,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: h43, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
-+ if (!preview || typeof h43 !== "function") return null;
++function RoomInvitePreview({ h: h45, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
++ if (!preview || typeof h45 !== "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 h43(
++ return h45(
+ "div",
+ { className: "fs-preview", "data-flybot": at, role: "status" },
-+ h43("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
-+ preview.lead ? h43("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
-+ preview.people ? h43("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
-+ preview.expiresAt ? h43("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
-+ onJoin || onClear ? h43(
++ h45("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
++ preview.lead ? h45("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
++ preview.people ? h45("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
++ preview.expiresAt ? h45("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
++ onJoin || onClear ? h45(
+ "div",
+ { className: "fs-preview-foot", "data-flybot": child("foot") },
-+ showClear && onClear ? h43("button", {
++ showClear && onClear ? h45("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm",
+ "data-flybot": child("clear"),
+ disabled: !!busy,
+ onClick: onClear
+ }, clearLabel) : null,
-+ onJoin ? h43("button", {
++ onJoin ? h45("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm fs-btn-primary",
+ "data-flybot": child("join"),
@@ -2473,41 +1954,7 @@ diff --git a/client.js b/client.js
+ const total = Math.max(1, Math.ceil(left / 1e3));
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`;
+}
-
--.bt-panel{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;width:var(--bt-panel-w,320px);pointer-events:auto;z-index:40;background:var(--bt-main);border-left:.5px solid var(--bt-line-2);display:flex;flex-direction:column;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-sizing:border-box;animation:bt-pane-in .3s cubic-bezier(.2,0,0,1);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-panel[data-leaving]{transform:translateX(100%);transition:transform .3s cubic-bezier(.2,0,0,1);pointer-events:none}
--.bt-panel[data-instant]{transition:none}
--@keyframes bt-pane-in{from{transform:translateX(100%)}}
--[class*="_centerCol"]{transition:margin-right ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
--.bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none}
--.bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)}
--.bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize}
--.bt-panel-view{flex:1;min-height:0;display:flex;flex-direction:column}
--.bt-drawer-top{display:flex;justify-content:flex-end;gap:8px;padding:12px 17px 6px 12px}
--.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
--.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
--.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
--.bt-drawer-name:hover{background:var(--bt-hover)}
--.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
--.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
--.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0}
--.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap}
--.bt-tab:hover{color:var(--bt-ink)}
--.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)}
--.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)}
--.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent}
--.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px}
--.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px}
--.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px}
--.bt-muted{color:var(--bt-ink-2)}
--.bt-swatches{display:flex;flex-wrap:wrap;gap:6px}
--.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)}
--.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)}
--.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical}
--.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
--.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)}
--.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums}
--.bt-danger{color:#e02135}
++
+// src/client/approval-claims.js
+var claims = /* @__PURE__ */ new Map();
+var claimListeners = /* @__PURE__ */ new Set();
@@ -2533,137 +1980,13 @@ diff --git a/client.js b/client.js
+ claimListeners.delete(fn);
+ };
+}
-
--/* 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}
++
+// src/shared/approval-waiting.js
+var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]);
+function isApprovalWaiting(status) {
+ return WAITING_STATUSES.includes(status);
+}
-
--/* 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/held-retry.js
+var TERMINAL = [
+ // A friend's Bot that is gone or no longer shared (plugins/social/src/social.js: botGoneError / notSharedError).
@@ -2683,15 +2006,7 @@ diff --git a/client.js b/client.js
+ const said = `${record.error ?? ""} ${record.code ?? ""}`;
+ return !TERMINAL.some((reason) => reason.test(said));
+};
-
--/* 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/flybot-slots.js
+var import_react2 = require("react");
+var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
@@ -2949,66 +2264,7 @@ diff --git a/client.js b/client.js
+.bt-flybot-approval-files { margin: 0 0 8px; padding-left: 18px; font-size: 12px; }
+.bt-file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 0 30px; }
+.bt-file-chip { font-size: 12px; padding: 3px 8px; border-radius: 8px; border: 1px solid var(--bt-line, rgba(0,0,0,.15)); background: transparent; cursor: pointer; }
-
--.bt-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-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
@@ -3037,15 +2293,48 @@ 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; }
+`;
-
--.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}
++
++// src/shared/turn-failures.js
++var FAILURE_LINES_SHOWN = 3;
++var failureLines = (snapshot, place, t2, limit = FAILURE_LINES_SHOWN) => {
++ const ended = Array.isArray(snapshot?.ended) ? snapshot.ended : [];
++ return ended.filter((entry) => entry?.silent === true && (entry.roomId ?? entry.sessionId) === place).slice(-limit).map((entry) => ({
++ id: `turn-failure-${entry.id}`,
++ text: t2("Could not run: {error}", { error: entry.failed ?? t2("the model call failed") }),
++ ...Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {}
++ }));
++};
++
++// src/client/turn-failures.js
++var import_react3 = require("react");
++function TurnFailures({ sessionId, work }) {
++ const [snapshot, setSnapshot] = (0, import_react3.useState)(() => work?.getSnapshot?.());
++ (0, import_react3.useEffect)(() => {
++ if (typeof work?.subscribe !== "function") return void 0;
++ const read = () => setSnapshot(work.getSnapshot());
++ const off = work.subscribe(read);
++ read();
++ return off;
++ }, [work]);
++ const lines = failureLines(snapshot, sessionId, t);
++ if (lines.length === 0) return null;
++ return (0, import_react3.createElement)(
++ "div",
++ { className: "bt-turn-failures", "data-flybot": "turn-failures" },
++ lines.map((line2) => (0, import_react3.createElement)(
++ "div",
++ { key: line2.id, className: "bt-turn-failure", "data-flybot": "turn-failure" },
++ (0, import_react3.createElement)("span", { className: "bt-turn-failure-text" }, line2.text),
++ line2.streak === void 0 ? null : (0, import_react3.createElement)("span", { className: "bt-turn-failure-streak" }, ` ${t("{count} turns in a row", { count: line2.streak })}`)
++ ))
++ );
++}
++var TURN_FAILURES_CSS = `
++.bt-turn-failures{width:100%;max-width:min(992px,100%);margin:0 auto;padding:0 0 6px;box-sizing:border-box}
++.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}
++`;
++
+// src/client/themes.js
+var THEME_KEYS = ["bg", "sidebar", "bubble", "card", "text", "user", "userText", "accent"];
+var THEMES = {
@@ -3179,237 +2468,113 @@ diff --git a/client.js b/client.js
+}
+
+// src/client/motion.js
-+var import_react3 = require("react");
-+var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
-+function springEasing(stiffness, damping) {
-+ const w0 = Math.sqrt(stiffness);
-+ const zeta = damping / (2 * w0);
-+ const at = (t2) => {
-+ if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2);
-+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
-+ return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2));
-+ };
-+ let end = 0.05;
-+ while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01;
-+ const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4));
-+ return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping };
-+}
-+var SPRING_SHAPE = springEasing(400, 31.2);
-+var SPRING_TAP = springEasing(900, 48);
-+var SPRING_LIST = springEasing(484, 35.2);
-+var SPRING_STRETCH = springEasing(320, 20);
-+var SPRING_POP = springEasing(500, 22);
-+var SPRING_LEAD = springEasing(784, 48.2);
-+var SPRING_TRAIL = springEasing(289, 29.2);
-+var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) };
-+var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(",");
-+function liveSprings(apply2) {
-+ const values = /* @__PURE__ */ new Map();
-+ let frame = 0;
-+ let last = 0;
-+ const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x]));
-+ const step = (now) => {
-+ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3));
-+ last = now;
-+ const steps = Math.ceil(dt * 240);
-+ const h43 = 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) * h43;
-+ value.x += value.v * h43;
-+ }
-+ 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_react3.useRef)(null);
-+ const box = given ?? ownBox;
-+ const thumb = (0, import_react3.useRef)(null);
-+ const own = (0, import_react3.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 = "";
-+ };
-+ (0, import_react3.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_react3.useEffect)(() => () => {
-+ own.current.observer?.disconnect();
-+ own.current.springs.stop();
-+ }, []);
-+ return { box, thumb };
-+}
-+function Segmented({ label, value, className, children }) {
-+ const glide = useGlide(value);
-+ return (0, import_react3.createElement)(
-+ "div",
-+ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label },
-+ (0, import_react3.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_react3.useRef)(null);
-+ (0, import_react3.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_react3.useRef)({ key, height: null });
-+ (0, import_react3.useLayoutEffect)(() => {
-+ const node = box.current;
-+ const state = last.current;
-+ if (!node) {
-+ last.current = { key, height: null };
-+ return;
-+ }
-+ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph");
-+ if (key === state.key) {
-+ if (running.length === 0) state.height = node.offsetHeight;
-+ return;
-+ }
-+ const from = running.length ? node.offsetHeight : state.height;
-+ for (const animation2 of running) animation2.cancel();
-+ const to = node.offsetHeight;
-+ last.current = { key, height: to };
-+ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return;
-+ node.dataset.morph = "";
-+ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" });
-+ animation.onfinish = () => {
-+ delete node.dataset.morph;
-+ };
-+ animation.oncancel = () => {
-+ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph;
-+ };
-+ });
-+}
-+function useLinger(value, ms) {
-+ const [kept, setKept] = (0, import_react3.useState)(value);
-+ (0, import_react3.useEffect)(() => {
-+ if (value) {
-+ setKept(value);
-+ return void 0;
-+ }
-+ const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms);
-+ return () => clearTimeout(timer);
-+ }, [value]);
-+ return [value || kept, !value && Boolean(kept)];
-+}
-+
-+// src/client/styles.js
-+var SHELL_CSS = `
-+body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)}
-+body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)}
-+${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")}
-+${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")}
-+.bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
-+.bt-mark svg{width:100%;height:100%;overflow:visible}
-+.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
++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 +3308,12 @@
+ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3));
+ last = now;
+ const steps = Math.ceil(dt * 240);
+- const h34 = dt / steps;
++ const h45 = 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) * h45;
++ value.x += value.v * h45;
+ }
+ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
+ value.x = value.to;
+@@ -1699,10 +3348,10 @@
+ }
+ var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]';
+ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) {
+- const ownBox = (0, import_react.useRef)(null);
++ 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 thumb = (0, import_react4.useRef)(null);
++ const own = (0, import_react4.useRef)(null);
+ if (own.current === null) {
+ own.current = {
+ key: void 0,
+@@ -1736,7 +3385,7 @@
+ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion());
+ container.dataset.glide = "";
+ };
+- (0, import_react.useLayoutEffect)(() => {
++ (0, import_react4.useLayoutEffect)(() => {
+ const state = own.current;
+ const node = box.current;
+ if (node === state.node && key === state.key) return;
+@@ -1750,7 +3399,7 @@
+ }
+ place(fresh);
+ });
+- (0, import_react.useEffect)(() => () => {
++ (0, import_react4.useEffect)(() => () => {
+ own.current.observer?.disconnect();
+ own.current.springs.stop();
+ }, []);
+@@ -1758,16 +3407,16 @@
+ }
+ function Segmented({ label, value, className, children }) {
+ const glide = useGlide(value);
+- return (0, import_react.createElement)(
++ return (0, import_react4.createElement)(
+ "div",
+ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label },
+- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ children
+ );
+ }
+ function useFlip(list, key, selector, id = (node) => node.dataset.flip) {
+- const tops = (0, import_react.useRef)(null);
+- (0, import_react.useLayoutEffect)(() => {
++ const tops = (0, import_react4.useRef)(null);
++ (0, import_react4.useLayoutEffect)(() => {
+ const container = list.current;
+ if (!container) return;
+ const next = /* @__PURE__ */ new Map();
+@@ -1788,8 +3437,8 @@
+ }, [key]);
+ }
+ function useMorph(box, key) {
+- const last = (0, import_react.useRef)({ key, height: null });
+- (0, import_react.useLayoutEffect)(() => {
++ const last = (0, import_react4.useRef)({ key, height: null });
++ (0, import_react4.useLayoutEffect)(() => {
+ const node = box.current;
+ const state = last.current;
+ if (!node) {
+@@ -1817,8 +3466,8 @@
+ });
+ }
+ function useLinger(value, ms) {
+- const [kept, setKept] = (0, import_react.useState)(value);
+- (0, import_react.useEffect)(() => {
++ const [kept, setKept] = (0, import_react4.useState)(value);
++ (0, import_react4.useEffect)(() => {
+ if (value) {
+ setKept(value);
+ return void 0;
+@@ -1838,6 +3487,7 @@
+ .bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
+ .bt-mark svg{width:100%;height:100%;overflow:visible}
+ .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
+.bt-mark-emoji{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;user-select:none}
-+.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
-+.bt-mark .bt-b{transform-origin:50% 100%}
-+.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
-+/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a
-+ mark only for its state; normal, the default, adds petting, pokes and eyes that follow
-+ the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */
-+body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch}
-+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite}
-+@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}}
-+@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}}
-+@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}}
-+@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
-+body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
-+body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
+ .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
+ .bt-mark .bt-b{transform-origin:50% 100%}
+ .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
+@@ -1856,6 +3506,14 @@
+ @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
+ body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
+ body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
+.bt-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}
@@ -3418,83 +2583,14 @@ diff --git a/client.js b/client.js
+.bt-tree-chev{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:16px;line-height:1;transition:transform .12s ease}
+.bt-tree-toggle[aria-expanded=false] .bt-tree-chev{transform:rotate(-90deg)}
+.bt-tree-leaf{cursor:default}
-+@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
-+/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
-+ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
-+ closed in a happy arc, and springs back when the pointer leaves. */
-+:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%}
-+.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out}
-+.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in}
-+.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none}
-+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s}
-+@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}}
-+.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out}
-+.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out}
-+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none}
-+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none}
-+.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite}
-+.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite}
-+.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite}
-+.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite}
-+.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite}
-+.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%}
-+.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite}
-+.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite}
-+.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite}
-+.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%}
-+.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite}
-+/* A question for you hops twice when it arrives and the badge keeps it in view after;
-+ a lively mark keeps hopping. */
-+.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
-+.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite}
-+.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2}
-+body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite}
-+.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1}
-+.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%}
-+@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}}
-+.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important}
-+.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important}
-+.bt-mark[data-poke]{cursor:pointer}
-+body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important}
-+body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
-+@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}}
-+@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}}
-+@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}}
-+@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}}
-+@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}}
-+@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}}
-+@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}}
-+@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}}
-+@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}}
-+@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}}
-+@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}}
-+@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}}
-+@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}}
-+/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */
-+@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}}
-+@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}}
-+@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}}
-+.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)}
-+.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))}
-+.bt-badge-admin svg{width:7px;height:7px}
-+.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none}
-+.bt-badge-main svg{width:100%;height:100%;overflow:visible}
-+.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke}
-+.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px}
-+.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0}
-+.bt-cluster>*{position:absolute}
-+.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums}
-+.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)}
-+.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0}
-+.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)}
-+.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important}
-+
-+.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
-+.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
+ @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
+ /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
+ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
+@@ -1933,15 +3591,23 @@
+ .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+ .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
+ .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
+-.bt-brand-whale{display:inline-flex;color:var(--bt-accent)}
+.bt-brand-mark{display:inline-flex;line-height:0}
+.bt-flybot-mark{display:block;flex:none}
+.bt-flybot-mark .bt-flybot-wing{fill:#8FB2FF}
@@ -3503,492 +2599,139 @@ diff --git a/client.js b/client.js
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-wing{fill:#A9C2FF}
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-body{fill:#6E87FF}
+[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-eye{fill:#0F1530}
-+.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
-+.bt-round:hover{background:var(--bt-hover)}
-+.bt-round:active{transform:scale(.94)}
-+.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
-+.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
+ .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease}
+ .bt-round:hover{background:var(--bt-hover)}
+ .bt-round:active{transform:scale(.94)}
+ .bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto}
+ .bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px}
+-.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
+.bt-tile{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease}
-+.bt-tile:hover{background:var(--bt-hover)}
-+.bt-tile[aria-current=page]{background:var(--bt-active)}
+ .bt-tile:hover{background:var(--bt-hover)}
+ .bt-tile[aria-current=page]{background:var(--bt-active)}
+.bt-tile-unread{position:absolute;top:8px;right:12px}
-+.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
-+.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
-+.bt-row:hover{background:var(--bt-hover)}
-+.bt-row[aria-current=page]{background:var(--bt-active)}
-+.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
-+.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
-+.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis}
+ .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease}
+@@ -1950,27 +3616,32 @@
+ .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
+ .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
+ .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-row-title.bt-row-title-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;overflow:hidden}
+.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)}
-+.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-+.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-+.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
-+.bt-unread-wait{background:var(--bt-star)}
+ .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)}
+ .bt-unread-wait{background:var(--bt-star)}
+.bt-shared-from,.bt-shared-wait{display:flex;flex-direction:column;gap:6px;padding:8px 0}
+.bt-shared-wait .bt-set-row{min-width:0}
+.bt-shared-wait .bt-set-copy{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
-+.bt-rail{align-items:center;padding:0 0 8px}
-+.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
-+.bt-side-foot{display:flex;padding-top:8px}
-+.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
-+.bt-connect:hover{background:var(--bt-hover)}
-+.bt-connect svg{flex:none;width:15px;height:15px}
+ .bt-rail{align-items:center;padding:0 0 8px}
+ .bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px}
+ .bt-side-foot{display:flex;padding-top:8px}
+ .bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease}
+ .bt-connect:hover{background:var(--bt-hover)}
+ .bt-connect svg{flex:none;width:15px;height:15px}
+-.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}
+-.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
+-.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
+.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}ms ${SPRING_SHAPE.easing}}
-+@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
+ @keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
+-@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}}
+@media (prefers-reduced-motion:reduce){}
-+
-+.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
-+.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
-+.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
-+.bt-pill:active{transform:scale(.965)}
-+.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
+
+ .bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none}
+ .bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}}
+ .bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)}
+ .bt-pill:active{transform:scale(.965)}
+ .bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
+.bt-pill:has(.bt-pill-name-face){height:auto;min-height:40px;align-items:flex-start}
+.bt-pill-name.bt-pill-name-face{min-width:0;white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
-+.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
-+.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
-+.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
-+.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)}
-+.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms}
-+.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)}
-+.bt-pill-mem-ico svg{width:16px;height:16px}
-+.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both}
-+@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}}
-+.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
-+@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}}
-+.bt-swap{color:var(--bt-ink-3);font-size:12px}
-+.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease}
-+.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
-+.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none}
-+.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)}
-+.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)}
-+@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}}
-+
-+.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
-+.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
+ .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}}
+ .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}}
+ .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}}
+@@ -1992,6 +3663,7 @@
+
+ .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
+ .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
+.bt-event.bt-not-sent{gap:6px;color:var(--bt-ink-3)}
-+.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
-+.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
-+.bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
-+.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important}
-+.bt-bubble p{margin:0}
-+.bt-bubble p + p{margin-top:6px}
-+.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px}
-+.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap}
-+.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat}
-+.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0}
-+.bt-group-msg:first-child{margin-top:0}
-+.bt-lead{flex:none;display:flex;margin-bottom:1px}
-+button.bt-lead{border:0;background:none;padding:0;cursor:pointer}
-+/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */
-+.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px}
-+.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px}
-+.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+
-+.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
-+body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)}
-+@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}}
-+.bt-q-settled{animation:none}
-+.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0}
-+.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
-+.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere}
-+.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
-+.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-q-x svg{width:14px;height:14px}
-+.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
-+.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px}
-+.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease}
-+.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)}
-+.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px}
-+.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none}
-+button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0}
-+.bt-q-raised .bt-q-opt::before{display:none}
-+button.bt-q-opt:hover{background:var(--bt-hover)}
-+button.bt-q-opt:active{background:var(--bt-active)}
-+.bt-q-opt[aria-pressed=true]{background:var(--bt-active)}
-+.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)}
-+.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease}
-+.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px}
-+/* With a description the key spans the two text lines less 2px above and below. */
-+.bt-q-opt:has(.bt-q-desc){--bt-key:34px}
-+.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px}
-+.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0}
-+.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere}
-+.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere}
-+.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
-+.bt-q-settled .bt-q-check{animation:none}
-+.bt-q-check svg{width:16px;height:16px}
-+.bt-q-settled .bt-key{opacity:.4}
-+.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)}
-+.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)}
-+.bt-q.bt-q-settled{gap:6px;padding:12px 16px}
-+.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500}
-+.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0}
-+.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0}
-+.bt-q-settled .bt-q-opt{padding:4px 8px}
-+.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box}
-+.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0}
-+.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease}
-+.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)}
-+.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere}
-+.bt-q-input:not(:placeholder-shown){field-sizing:content}
-+.bt-q-input::placeholder{color:var(--bt-ink-3)}
-+.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards}
-+@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}}
-+.bt-q-submit:hover:not(:disabled){opacity:.86}
-+.bt-q-submit:disabled{opacity:.4;cursor:default}
-+.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-q-btn:hover{background:var(--bt-active)}
-+.bt-q-foot{display:flex;justify-content:flex-end;gap:8px}
-+.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)}
-+.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px}
-+.bt-q-raised .bt-q-head:last-child{padding-bottom:16px}
-+.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px}
-+.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)}
-+.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)}
-+.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)}
-+.bt-q-raised .bt-q-body{padding:2px 6px 0}
-+.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)}
-+.bt-q-ownrow{cursor:text}
-+.bt-q-ownrow .bt-q-input{color:var(--bt-ink)}
-+.bt-q-raised .bt-q-foot{padding:0 8px 8px}
-+@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
-+
-+.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
+ .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
+ .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
+ .bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
+@@ -2085,6 +3757,7 @@
+ @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
+
+ .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
+.bt-pane[data-menu]{background:transparent}.bt-pane[data-menu] .bt-newchat{flex:none;align-self:flex-start;width:min(280px,calc(100% - 16px));max-height:calc(100% - 16px);margin:8px;overflow:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.06)}.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:0;box-shadow:none;border:0;max-width:none}
-+.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
-+.bt-to-label{color:var(--bt-ink-2)}
-+.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
-+.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px}
-+.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px}
-+.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out}
-+.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%}
-+.bt-option[aria-selected=true]{background:var(--bt-active)}
-+.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
-+.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
+ .bt-to-label{color:var(--bt-ink-2)}
+ .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
+@@ -2098,7 +3771,9 @@
+ .bt-option[aria-selected=true]{background:var(--bt-active)}
+ .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
+ .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+-.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out}
+.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:0 1 auto;min-width:0;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;animation:bt-veil-in .16s ease-out}
+.bt-option-copy{flex:1;min-width:0;display:flex;flex-direction:column}
+.bt-option-sub{font-size:11px;line-height:15px;color:var(--bt-ink-2);white-space:normal;overflow-wrap:anywhere}
-+.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
-+.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
-+.bt-nc-spacer{flex:1}
-+.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center}
-+.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box}
-+.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none}
-+.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box}
-+.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
-+.bt-nc-send:disabled{opacity:.3;cursor:default}
-+.bt-create{flex:1;display:flex;flex-direction:column;min-width:0}
-+.bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box}
-+.bt-create-gap{width:28px}
-+.bt-create-title{font-size:14px;line-height:20px;font-weight:500}
-+.bt-create-body{flex:1;overflow-y:auto;padding:28px 16px 32px;display:flex;justify-content:center}
-+.bt-create-card{width:100%;max-width:400px;display:flex;flex-direction:column;gap:18px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .22s ease-out both}
-+.bt-create-preview{display:flex;flex-direction:column;align-items:center;gap:10px}
-+.bt-create-pop{display:inline-flex;animation:bt-create-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} both}
-+@keyframes bt-create-pop{from{transform:scale(.72);opacity:.4}}
-+.bt-create-name{font-size:17px;line-height:24px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-create-step{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-bottom:-10px}
-+.bt-create-looks{display:flex;flex-direction:column;gap:10px;padding:10px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)}
-+.bt-create-lookbar{justify-content:center}
-+.bt-create-foot{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}
-+.bt-create-go{height:36px;padding:0 20px}
-+@media (prefers-reduced-motion:reduce){.bt-create-card,.bt-create-pop{animation:none}}
-+.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
-+.bt-send:disabled{opacity:.35;cursor:default}
-+.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
+ .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
+ .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
+ .bt-nc-spacer{flex:1}
+@@ -2127,6 +3802,10 @@
+ .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
+ .bt-send:disabled{opacity:.35;cursor:default}
+ .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
+.bt-fix-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
+.bt-fix-btn{font:inherit;font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer}
+.bt-fix .bt-note{margin-top:6px}
+.bt-fix-link{margin-top:6px;color:inherit;user-select:all}
-+
-+.bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
-+.bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0}
-+.bt-dialog .bt-head{height:62px}
-+.bt-dialog-log{flex:1;overflow-y:auto;padding:8px 16px 20px;width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;box-sizing:border-box}
-+.bt-dialog-foot{display:flex;justify-content:center;padding:10px 10px 16px}
-+.bt-exchange{animation:bt-sheet-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} both,bt-veil-in .24s ease-out both}
-+.bt-exchange[data-leaving]{animation:bt-sheet-out .22s cubic-bezier(.4,0,1,1) forwards;pointer-events:none}
-+@keyframes bt-sheet-in{from{transform:translateY(24px)}}
-+@keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}}
-+.bt-exchange .bt-dialog{position:relative}
-+.bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin}
-+.bt-exchange .bt-tools{display:none}
-+.bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both}
-+.bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none}
-+.bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards}
-+@keyframes bt-foot-in{from{opacity:0;transform:translateY(6px)}}
-+@keyframes bt-foot-out{to{opacity:0;transform:translateY(6px)}}
-+@media (prefers-reduced-motion:reduce){.bt-exchange,.bt-exchange .bt-dialog-foot{animation-duration:.01s}}
-+.bt-soft{height:36px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);padding:0 16px;font-size:14px;cursor:pointer}
-+.bt-soft:hover{background:var(--bt-active)}
-+.bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0}
-+
-+.bt-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-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 +3842,8 @@
+ .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
+ .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
+ .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.bt-drawer-name-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;text-overflow:ellipsis;line-clamp:3;cursor:default}
+.bt-drawer-name-face:hover{background:none}
-+.bt-drawer-name:hover{background:var(--bt-hover)}
-+.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
-+.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
-+.bt-tabs{position:relative;display:flex;justify-content:center;gap:2px;margin:16px 16px 0}
-+.bt-tab{position:relative;z-index:1;border:0;background:none;border-radius:9999px;padding:4px 8px;font-size:13px;line-height:20px;color:var(--bt-ink-2);cursor:pointer;white-space:nowrap}
-+.bt-tab:hover{color:var(--bt-ink)}
-+.bt-tab[aria-selected=true]{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-tabs>.bt-thumb{top:0;bottom:0;border-radius:9999px;background:var(--bt-hover)}
-+.bt-tabs[data-glide] .bt-tab[aria-selected=true]{background:transparent}
-+.bt-drawer-body{flex:1;overflow-y:auto;padding:20px 16px 16px;font-size:13px;line-height:18px;display:flex;flex-direction:column;gap:16px}
-+.bt-empty{text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:8px 12px}
-+.bt-section-title{font-size:12px;color:var(--bt-ink-2);margin-bottom:6px}
-+.bt-muted{color:var(--bt-ink-2)}
-+.bt-swatches{display:flex;flex-wrap:wrap;gap:6px}
-+.bt-swatch{width:22px;height:22px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px var(--bt-line-2)}
-+.bt-swatch[aria-pressed=true]{border-color:var(--bt-ink)}
-+.bt-textarea{width:100%;box-sizing:border-box;min-height:120px;border:1px solid var(--bt-line);border-radius:8px;padding:8px;background:var(--bt-card);color:var(--bt-ink);font:inherit;resize:vertical}
-+.bt-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
-+.bt-kv{display:flex;justify-content:space-between;gap:8px;font-size:12px;line-height:20px;color:var(--bt-ink-2)}
-+.bt-kv span:last-child{color:var(--bt-ink);font-variant-numeric:tabular-nums}
-+.bt-danger{color:#e02135}
-+
-+/* Bot settings: fields, the avatar editor popover, and the details-pane sub-view. */
-+.bt-field{display:flex;flex-direction:column;gap:6px}
-+.bt-field-label{font-size:13px;line-height:18px;font-weight:500}
-+.bt-input{width:100%;box-sizing:border-box;height:36px;border:1px solid var(--bt-line);border-radius:8px;padding:0 10px;background:var(--bt-card);color:var(--bt-ink);font:inherit;font-size:13px;outline:none}
-+.bt-input:focus{border-color:var(--bt-line-2)}
-+.bt-input-area{height:auto;min-height:96px;padding:8px 10px;line-height:18px;resize:vertical}
-+.bt-note{font-size:12px;line-height:16px;color:var(--bt-ink-3)}
-+.bt-soft-sm{height:28px;padding:0 12px;font-size:12px}
-+.bt-set-avatar{display:flex;justify-content:center;padding:4px 0}
-+.bt-avatar-anchor{position:relative}
-+.bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0}
-+.bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)}
-+.bt-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-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 +3936,7 @@
+ .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)}
+ .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent}
+ .bt-settings-shell{margin-top:auto}
+.bt-settings-out{margin-left:auto;opacity:.6;font-size:12px}
-+/* Usage while another page is open: out of the flow at Usage's own width, so it keeps
-+ its layout, and not rendered. */
-+.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
-+/* The page column takes the dialog's final size at once, so while the dialog resizes
-+ only its edge moves and the page does not lay out again every frame. */
-+.bt-settings-main{flex:none;display:flex;flex-direction:column;min-width:0;width:calc(var(--bt-set-w) - 198px);height:var(--bt-set-h)}
-+.bt-settings-head{display:flex;align-items:center;gap:6px;padding:16px 56px 4px 20px}
-+.bt-settings-head h2{margin:0;font-size:15px;line-height:24px;font-weight:600;animation:bt-veil-in .2s ease-out}
-+.bt-settings-head>.bt-icon-btn{animation:bt-back-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
-+@keyframes bt-back-in{from{translate:6px 0;scale:.8}}
-+.bt-settings-scroll{flex:1;overflow-y:auto;padding:12px 20px 24px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin}
-+.bt-settings-page>:not(.bt-us){animation:bt-page-up ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .22s ease-out backwards}
-+.bt-settings-page[data-move=in]>:not(.bt-us){animation-name:bt-page-in,bt-veil-in}
-+.bt-settings-page[data-move=out]>:not(.bt-us){animation-name:bt-page-out,bt-veil-in}
-+.bt-settings-page>:nth-child(2){animation-delay:30ms}
-+.bt-settings-page>:nth-child(3){animation-delay:60ms}
-+.bt-settings-page>:nth-child(4){animation-delay:90ms}
-+.bt-settings-page>:nth-child(n+5){animation-delay:120ms}
-+@keyframes bt-page-up{from{transform:translateY(10px)}}
-+@keyframes bt-page-in{from{transform:translateX(18px)}}
-+@keyframes bt-page-out{from{transform:translateX(-18px)}}
-+.bt-settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
-+.bt-settings-close:hover{background:var(--bt-active);color:var(--bt-ink)}
-+.bt-settings-form{display:flex;flex-direction:column;gap:16px}
-+.bt-set-section h3{font-size:13px;line-height:18px;font-weight:600;margin:0 0 8px;padding:0 4px}
-+.bt-set-card{border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card);display:flex;flex-direction:column}
-+.bt-set-card>*+*{border-top:.5px solid var(--bt-line)}
-+.bt-set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:none;color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;text-align:left;width:100%;box-sizing:border-box;border-radius:0}
-+button.bt-set-row{cursor:pointer}
-+button.bt-set-row:hover{background:var(--bt-hover)}
-+.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)}
-+.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px}
-+.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px}
+ /* Usage while another page is open: out of the flow at Usage's own width, so it keeps
+ its layout, and not rendered. */
+ .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
+@@ -2288,6 +3970,8 @@
+ .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}
-+.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-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 +3992,17 @@
+ .bt-connector-actions{flex:none;display:flex;gap:6px}
+ .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+ .bt-connector-form-row{display:flex;align-items:center;gap:6px}
+.bt-connector-apps{display:flex;flex-direction:column;gap:8px}
-+.bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)}
+ .bt-connector-link{flex:1;font-size:12px;color:var(--bt-ink-2)}
+.bt-connector-uses{padding-left:48px;font-size:12px;line-height:16px;color:var(--bt-ink-3)}
+.bt-connector-bots{display:flex;flex-direction:column;gap:2px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
+.bt-connector-others{padding-left:20px}
-+.bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135}
+ .bt-connector-error{padding-left:48px;font-size:12px;line-height:16px;color:#e02135}
+.bt-connector-app-list{list-style:none;margin:0;padding:0 0 0 48px;display:flex;flex-direction:column;gap:2px}
+.bt-connector-app{display:flex;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-2)}
+.bt-connector-app-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.bt-app-status{color:var(--bt-ink-3)}
+[data-status=sign-in]>.bt-app-status{color:#c46a00}
-+
-+/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the
-+ shell offers them and CSS-module name suffixes elsewhere. */
-+[class*="_logoRow"],[class*="_newSession"],nav[class*="_panelList"]{display:none!important}
-+[class*="_footArea"]{display:flex!important;flex-direction:row-reverse;align-items:center;gap:9px;padding:8px 6px 6px!important}
-+[class*="_footArea"]>[class*="_footerActions"]{flex:1 1 auto;min-width:0}
-+[class*="_footArea"]>[class*="_settingsArea"]{flex:none;width:auto!important}
-+[class*="_footArea"] button[aria-label="Settings"]{width:auto!important;padding:0!important;background:none!important;border-radius:50%!important;height:auto!important}
-+[class*="_widthHandle"]{display:none!important}
-+
-+.bt-voice-pane{flex-direction:column;align-items:center;justify-content:center;animation:bt-fade .2s ease-out}
-+.bt-voice-stage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:24px;max-width:560px;text-align:center}
-+.bt-voice-orb{position:relative;width:200px;height:200px;display:grid;place-items:center;margin-bottom:8px}
-+.bt-voice-ring{position:absolute;inset:34px;border-radius:50%;background:var(--bt-hover);transform:scale(calc(1 + var(--bt-level,0) * .55));transition:transform .08s linear}
-+.bt-voice-ring-2{inset:14px;background:none;box-shadow:inset 0 0 0 1px var(--bt-line-2);transform:scale(calc(.94 + var(--bt-level,0) * .3));opacity:.8}
-+.bt-voice-stage[data-phase=thinking] .bt-voice-ring{animation:bt-voice-breathe 1.6s ease-in-out infinite}
-+.bt-voice-stage[data-phase=speaking] .bt-voice-ring{animation:bt-voice-speak .9s ease-in-out infinite}
-+@keyframes bt-voice-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
-+@keyframes bt-voice-speak{0%,100%{transform:scale(1.04)}30%{transform:scale(1.2)}60%{transform:scale(1.1)}}
-+.bt-voice-orb>.bt-mark{position:relative}
-+.bt-voice-name{font-size:16px;line-height:22px;font-weight:500}
-+.bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px}
-+.bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
-+.bt-voice-note{font-size:12px;color:#e02135}
-+.bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease}
-+.bt-voice-setup:hover{background:var(--bt-active)}
-+.bt-voice-bar{display:flex;gap:16px;padding:0 0 32px}
-+.bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease}
-+.bt-voice-btn:hover{background:var(--bt-active)}
-+.bt-voice-btn:active{transform:scale(.94)}
-+.bt-voice-btn[aria-pressed=true]{background:var(--bt-user);color:var(--bt-user-ink)}
-+.bt-voice-slash{position:absolute;width:24px;height:2px;border-radius:1px;background:currentColor;transform:rotate(-45deg)}
-+.bt-voice-end{background:#ff3e51;color:#fff}
-+.bt-voice-end:hover{background:#e02135}
-+@keyframes bt-fade{from{opacity:0}to{opacity:1}}
-+@keyframes bt-blur-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}}
-+@media (prefers-reduced-motion:reduce){.bt-voice-pane,.bt-voice-caption,.bt-voice-ring{animation:none!important}}
-+.bt-msg{display:flex;flex-direction:column;align-items:flex-start;max-width:min(80%,560px,calc(100% - 82px))}
-+/* The bubble's row: reactions hang below it, so the hover toolbar (and a group
-+ sender's avatar) line up with the bubble alone. */
-+.bt-msg-line{position:relative;display:flex;align-items:flex-end;gap:8px;max-width:100%}
-+.bt-msg-line>.bt-bubble{max-width:none;min-width:0}
-+/* The same bubble width as beside an avatar column: 80% of (100% - 30px), plus the 30px. */
-+.bt-astack[data-lead]>.bt-msg{max-width:min(calc(80% + 6px),590px,calc(100% - 52px))}
-+.bt-tools{position:absolute;top:50%;left:100%;margin-left:6px;transform:translateY(-50%);display:flex;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s}
-+.bt-tools::after{content:"";position:absolute;top:-8px;bottom:-8px;right:100%;width:128px;z-index:-1}
-+.bt-msg:hover>.bt-msg-line>.bt-tools,.bt-msg:focus-within>.bt-msg-line>.bt-tools,.bt-tools[data-open]{opacity:1;visibility:visible}
-+.bt-tool{width:24px;height:24px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease}
-+.bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)}
-+.bt-tool:active{background:var(--bt-active)}
-+.bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
-+.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}
-@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side
+
+ /* 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 +4120,8 @@
.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}}
@@ -3998,206 +2741,10 @@ diff --git a/client.js b/client.js
.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
-@@ -2488,2292 +2488,4092 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
- @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}}
-+/* 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}
+@@ -2599,6 +4293,14 @@
+ .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))}
@@ -4206,28 +2753,27 @@ diff --git a/client.js b/client.js
+/* A person's typed line keeps its line breaks, as on the sender's side (#455): Markdown would join them. */
+.bt-person-msg .bt-bubble :is(p,li){white-space:pre-line}
+.bt-remote-handle{display:inline-flex;align-items:center;gap:3px;margin-left:6px;font-weight:400;color:var(--bt-ink-3,rgba(20,20,20,.36))}
-+.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
-+.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
-+.bt-event>span{padding:0 2px}
-+.bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}
-+.bt-event button:hover{background:var(--bt-hover)}
-+.bt-event button:active{background:var(--bt-active)}
-+.bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px}
-+.bt-event-whole>*{flex:none}
-+.bt-event-whole>span:first-child{margin-right:2px}
-+/* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */
-+.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}
-+[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap}
-+.bt-group-replies{display:contents}
-+/* A Bot chat is flat in every work-details mode, without changing the user's mode
-+ (it is a DSH-wide setting): step groups lose their header and stay open, and what a
-+ mode folds away in a finished turn stays shown. TurnProcessCell also opens that
-+ fold; these rules cover the first frame before it does. */
-+[data-step-process]>div:first-child{display:none!important}
-+[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
-+:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
-+[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
+ .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
+ .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
+ .bt-event>span{padding:0 2px}
+@@ -2612,7 +4314,6 @@
+ .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 +4323,28 @@
+ [data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
+ :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
+ [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
+-[class*="_header"]:has(.bt-head){border-bottom-color:transparent!important;box-shadow:none!important}
+-/* The transcript runs under a floating header pill. */
+-header[class*="_header"]:has(.bt-head){position:absolute!important;top:0;left:0;right:0;z-index:8;display:flex!important;padding:10px 0 0!important;background:transparent!important;pointer-events:none;justify-content:center}
+-header[class*="_header"]:has(.bt-head)::before{content:"";position:absolute;inset:0 0 auto;height:60px;background:linear-gradient(to bottom,var(--bt-main),color-mix(in srgb,var(--bt-main) 0%,transparent));pointer-events:none;z-index:-1}
+-header[class*="_header"]:has(.bt-head)>[class*="_headerLeading"]{position:absolute;left:12px;top:10px;pointer-events:auto}
+-header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
+/* FlyBot's pill rides the shell's own Session header utilities, beside the seats
+ the native header renders (actions: the subagent catalog; lineage; corner).
+ It is a compact control in that row now, not a floating replacement for the
@@ -4240,242 +2786,106 @@ 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. */
+ [class*="_body"]:has(> [class*="_scrollBody"]){--dsh-chat-content-width:min(960px,100%)}
+ /* DSH's turn tail keeps what plugins put in it (delivered files, the Bot's separators),
+ without its copy and branch row; a tail with nothing in it takes no room. */
+-[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]){display:none!important}
+-[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))){display:none!important}
+[data-turn-tail]>:not([data-slot="conversation.chat.turnTail"]):not(.bt-typing){display:none!important}
+[class*="_flowItem"][data-chat-flow-kind="turn-tail"]:not(:has([data-slot="conversation.chat.turnTail"]:not(:empty))):not(:has([data-turn-tail]>.bt-typing)){display:none!important}
-+/* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not
-+ the shell's new-session hero: no headline, no workspace or mode picker, and the
-+ composer stays docked at the bottom. */
-+[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
+ /* Every team Session belongs to a Bot or a group, so a blank one is an empty chat, not
+ the shell's new-session hero: no headline, no workspace or mode picker, and the
+ composer stays docked at the bottom. */
+ [data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
+body[data-bt-remote-chat=empty][data-content-phase=hero] [class*="_scrollBody"]::before{content:var(--bt-remote-empty);display:block;text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:16px 12px 8px;max-width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto;white-space:pre-wrap}
-+[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
-+[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
-+[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
-+/* The right sides give back the scrollbar gutter so both edges sit 16px from the column. */
-+[class*="_scroll"]:has(> [class*="_column"]){padding:60px 11px 16px 16px!important}
-+/* The shell's running status only hosts the activity row (.bt-typing). */
-+[data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important}
-+[data-chat-running]>span:not([role=status]){display:none!important}
-+[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important}
-+.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}
+@@ -2648,6 +4356,9 @@
+ [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))}
-+.bt-typing>.bt-mark .bt-eyes{transition:scale .28s cubic-bezier(.4,.06,.18,1)}
-+.bt-typing>.bt-mark:hover .bt-eyes{scale:1.18}
-+.bt-typing-text{display:flex;align-items:center;flex:1 1 auto;min-width:0;animation:bt-typing-label .22s ease-out .2s backwards}
-+.bt-typing-label{display:inline-flex;align-items:center;gap:6px;min-width:0;animation:bt-typing-label .22s ease-out backwards}
-+.bt-typing-label>.bt-shimmer{font-size:14px;line-height:22px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-+.bt-typing-time{flex-shrink:0;font-size:12px;line-height:22px;color:var(--bt-ink-3);white-space:nowrap}
-+@keyframes bt-typing-in{from{opacity:0;transform:scale(.9)}}
-+@keyframes bt-typing-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.92);filter:blur(3px)}}
-+@keyframes bt-typing-label{from{opacity:0;filter:blur(5px);transform:translateY(3px)}}
-+/* Shimmer: a stepped sweep clipped to the text. */
-+.bt-shimmer{color:transparent;background:linear-gradient(90deg,var(--bt-shimmer-base) 0%,var(--bt-shimmer-base) 25%,var(--bt-ink) 60%,var(--bt-shimmer-base) 75%,var(--bt-shimmer-base) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;animation:bt-shimmer 2.2s steps(60) infinite;--bt-shimmer-base:color-mix(in srgb,var(--bt-ink) 40%,transparent)}
-+@keyframes bt-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
-+/* New rows slide up; a reply that replaces the activity row grows out of its corner. */
-+[data-bt-enter="new"]{animation:bt-row-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}}
-+[data-bt-enter="after-collapse"]{transform-origin:0 100%;animation:bt-row-after .24s cubic-bezier(.23,1,.32,1) .38s backwards}
-+@keyframes bt-row-in{from{transform:translateY(12px)}to{transform:none}}
-+@keyframes bt-row-after{0%{opacity:0;transform:translateY(12px) scale(.94)}55%{opacity:1}100%{opacity:1;transform:none}}
-+@media (prefers-reduced-motion:reduce){
-+.bt-typing,.bt-typing[data-exiting]{animation-name:bt-fade-in;animation-duration:.12s;transform:none}
-+.bt-typing[data-exiting]{animation-name:bt-fade-out}
-+.bt-typing-text,.bt-typing-label{animation-name:bt-fade-in;animation-delay:0s}
-+.bt-shimmer{animation:none;background:none;color:var(--bt-ink-2)}
-+[data-bt-enter="new"]{animation:none}
-+[data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out}
-+}
-+@keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
-+@keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
-+[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
+ .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0}
+ .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out}
+ .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}
+@@ -2679,7 +4390,8 @@
+ @keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
+ @keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
+ [data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
+-[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
+[data-composer-card]:hover{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
+[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 55%,transparent)!important}
-+[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
-+[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
-+[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
-+[data-composer-card]>[class*="_row"]{display:contents!important}
-+[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0}
-+[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important}
-+[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0}
-+[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%}
-+[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
-+[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
-+[data-composer-placeholder]{font-size:0!important;line-height:0!important}
+ [data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
+ [class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
+ [class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
+@@ -2691,6 +4403,8 @@
+ [data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
+ [data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
+ [data-composer-placeholder]{font-size:0!important;line-height:0!important}
+/* A friend's Bot chat: no local run stats (they are relay turns, always 0; #419). */
+body[data-bt-friend-chat] [data-composer-stats]{display:none!important}
-+[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
-+/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
-+[data-composer-card] button[class*="_add"]{display:none!important}
-+.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
-+.bt-plus:active{scale:.9}
-+.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}}
-+.bt-plus[aria-expanded=true] svg{rotate:45deg}
-+.bt-plus-menu{min-width:180px}
-+[data-composer-card] [class*="_tools"]{gap:0!important}
-+[data-composer-card] [class*="_modes"]:empty{display:none!important}
-+[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important}
-+[data-composer-card] [class*="_trailing"]{gap:8px}
-+[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px}
-+[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9}
-+[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9}
-+@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}}
-+[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px}
-+[data-composer-card] button[class*="_primary"]:disabled{display:none!important}
-+[data-composer-dock]{display:none!important}
-+.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
-+.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)}
-+.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
-+.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite}
-+@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}}
-+.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
-+.bt-voice:hover{opacity:.9}
-+.bt-mic:active,.bt-voice:active{scale:.9}
-+[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
-+body[data-bt-room] .bt-voice{display:none!important}
+ [data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
+ /* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
+ [data-composer-card] button[class*="_add"]{display:none!important}
+@@ -2722,6 +4436,7 @@
+ .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}
-+.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}
+ 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 +4491,7 @@
+ .bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
+ .bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
+ .bt-urow{display:flex;justify-content:flex-end}
+.bt-urow>.bt-lead{align-self:flex-end;margin-left:6px}
-+.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
-+.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
-+.bt-ububble p{margin:0;line-height:22px}
-+.bt-uimages{display:block;font-size:12px;opacity:.7}
-+.bt-earlier .bt-event{margin:4px 0}
-+.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)}
-+.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)}
-+`;
-+function installStyles(text, tag) {
-+ const style = document.createElement("style");
-+ style.dataset.dshBot = tag;
-+ style.textContent = text;
-+ document.head.appendChild(style);
-+ return style;
-+}
-+var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g;
-+var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g;
-+function sheetClassNames(sheet) {
-+ const names = [];
-+ const visit = (rules) => {
-+ for (const rule of rules) {
-+ if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]);
-+ if (rule.cssRules) visit(rule.cssRules);
-+ }
-+ };
-+ try {
-+ visit(sheet.cssRules);
-+ } catch {
-+ }
-+ return names;
-+}
-+function compileFragmentSelectors(text, names) {
-+ const compiled = /* @__PURE__ */ new Map();
-+ return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => {
-+ if (!compiled.has(fragment)) {
-+ const exact = [];
-+ for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`);
-+ compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole);
-+ }
-+ return compiled.get(fragment);
-+ });
-+}
-+function installCompiledStyles(text, tag) {
-+ const style = installStyles("", tag);
-+ const cache = /* @__PURE__ */ new WeakMap();
-+ const render = () => {
-+ const names = /* @__PURE__ */ new Set();
-+ for (const sheet of document.styleSheets) {
-+ if (sheet.ownerNode?.dataset?.dshBot) continue;
-+ let list = cache.get(sheet);
-+ if (!list) {
-+ list = sheetClassNames(sheet);
-+ cache.set(sheet, list);
-+ }
-+ for (const name of list) names.add(name);
-+ }
-+ const next = compileFragmentSelectors(text, names);
-+ if (style.textContent !== next) style.textContent = next;
-+ };
-+ render();
-+ let timer;
-+ const schedule = () => {
-+ clearTimeout(timer);
-+ timer = setTimeout(render, 120);
-+ };
-+ const onLoad = (event) => {
-+ if (event.target instanceof HTMLLinkElement) schedule();
-+ };
-+ const observer = new MutationObserver(schedule);
-+ observer.observe(document.head, { childList: true });
-+ document.addEventListener("load", onLoad, true);
-+ return () => {
-+ observer.disconnect();
-+ document.removeEventListener("load", onLoad, true);
-+ clearTimeout(timer);
-+ style.remove();
-+ };
-+}
-+
+ .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
+ .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
+ .bt-ububble p{margin:0;line-height:22px}
+@@ -2855,226 +4571,204 @@
+ };
+ }
+
+-// src/client/mark.js
+-var import_react2 = require("react");
+-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"];
+-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle";
+-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working";
+-function useGaze(ref, enabled) {
+- (0, import_react2.useEffect)(() => {
+- if (!enabled) return void 0;
+- let frame = 0;
+- let point = null;
+- const place = () => {
+- frame = 0;
+- const node = ref.current;
+- if (!node || !point) return;
+- const box = node.getBoundingClientRect();
+- const clamp = (value) => Math.max(-1, Math.min(1, value));
+- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2));
+- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2));
+- };
+- const onMove = (event) => {
+- point = { x: event.clientX, y: event.clientY };
+- if (!frame) frame = requestAnimationFrame(place);
+- };
+- const onLeave = () => {
+- point = null;
+- ref.current?.style.setProperty("--bt-gx", "0");
+- ref.current?.style.setProperty("--bt-gy", "0");
+- };
+- window.addEventListener("pointermove", onMove, { passive: true });
+- document.documentElement.addEventListener("pointerleave", onLeave);
+- return () => {
+- cancelAnimationFrame(frame);
+- window.removeEventListener("pointermove", onMove);
+- document.documentElement.removeEventListener("pointerleave", onLeave);
+- };
+- }, [enabled]);
+// src/shared/face-title.js
+var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+function isGenericFaceTitle(name) {
@@ -4492,7 +2902,92 @@ diff --git a/client.js b/client.js
+function isAutoFaceTitle(name) {
+ const n = String(name ?? "").trim();
+ return isGenericFaceTitle(n) || n.startsWith("Face-to-face · ");
-+}
+ }
+-var FIDGETS = ["hop", "sway", "stretch", "look"];
+-function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) {
+- const look = given ?? lookOf(bot);
+- const ref = (0, import_react2.useRef)(null);
+- const maskId = (0, import_react2.useRef)("");
+- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`;
+- const [poked, setPoked] = (0, import_react2.useState)(false);
+- useGaze(ref, gaze);
+- const common = {
+- ref,
+- className: poked ? "bt-mark bt-mark-poke" : "bt-mark",
+- "data-shape": look.image ? "image" : shapeId(look.shape),
+- "data-state": STATES.includes(state) ? state : "idle",
+- "data-live": live ? "" : void 0,
+- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0,
+- "data-gaze": gaze ? "" : void 0,
+- "data-poke": pokeable ? "" : void 0,
+- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} },
+- role: label ? "img" : void 0,
+- "aria-label": label,
+- "aria-hidden": label ? void 0 : true,
+- onClick: pokeable ? () => setPoked(true) : void 0,
+- onAnimationEnd: poked ? (event) => {
+- if (/^bt-poke/.test(event.animationName)) setPoked(false);
+- } : void 0
+- };
+- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false }));
+- const box = shapeOf(look.shape).size;
+- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } }));
+-}
+-function StarIcon({ size = 8 }) {
+- return (0, import_react2.createElement)(
+- "svg",
+- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true },
+- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" })
+- );
+-}
+-function AdminStar() {
+- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 }));
+-}
+-function MainStar() {
+- return (0, import_react2.createElement)(
+- "svg",
+- { viewBox: "0 0 20 20", "aria-hidden": true },
+- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" })
+- );
+-}
+-function useSettled(state) {
+- const prev = (0, import_react2.useRef)(state);
+- const [settled, setSettled] = (0, import_react2.useState)(false);
+- (0, import_react2.useEffect)(() => {
+- const was = prev.current;
+- prev.current = state;
+- if (state !== "idle") {
+- setSettled(false);
+- return void 0;
+- }
+- if (was === "idle" || was === "alert") return void 0;
+- setSettled(true);
+- const timer = setTimeout(() => setSettled(false), 1800);
+- return () => clearTimeout(timer);
+- }, [state]);
+- return settled;
+-}
+-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) {
+- const settled = useSettled(state);
+- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size });
+- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null;
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } },
+- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }),
+- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null,
+- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null,
+- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null,
+- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null
+- );
+-}
+-function clusterSeats(count, frame) {
+- if (count === 1) return [[0, 0, frame]];
+- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]];
+- if (count === 3) {
+- const size = frame * 5 / 9;
+- const rest = frame - size;
+- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]];
+function ownersFromMembers(members) {
+ const rows = [];
+ const seen = /* @__PURE__ */ new Set();
@@ -4503,10 +2998,43 @@ diff --git a/client.js b/client.js
+ seen.add(key);
+ const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase();
+ rows.push({ owner, sort: handle || key });
-+ }
+ }
+- const half = frame / 2;
+- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
+ rows.sort((a, b) => a.sort.localeCompare(b.sort));
+ return rows.slice(0, 3).map((row) => row.owner);
-+}
+ }
+-function RoomAvatar({ room: room2, roster, size = 36 }) {
+- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
+- const people = members.length === 1 ? [members[0], null] : members;
+- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
+- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
+- if (size <= 20 && people.length > 1) {
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
+- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
+- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
+- );
+- }
+- const seats = clusterSeats(Math.min(people.length, 4), size);
+- const overflow = people.length > 4 ? people.length - 3 : 0;
+- const gap = size / 14;
+- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap }));
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-cluster", style: { width: size, height: size } },
+- seats.map(([x, y, s], index) => {
+- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`);
+- const style = { left: x, top: y, width: s, height: s };
+- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" });
+- if (overflow > 0 && index === 3) {
+- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`);
+- }
+- const bot = people[index];
+- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
+- })
+- );
+function uniqueOwners(owners) {
+ const seen = /* @__PURE__ */ new Set();
+ const out = [];
@@ -4611,1083 +3139,806 @@ diff --git a/client.js b/client.js
+ observer?.disconnect();
+ link.remove();
+ };
-+}
-+
-+// src/client/icons.js
-+var import_react4 = require("react");
-+function PlusIcon() {
-+ return (0, import_react4.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
-+}
-+function SearchIcon() {
-+ return (0, import_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react4.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
-+}
-+function PeopleIcon() {
-+ return (0, import_react4.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_react4.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
-+ (0, import_react4.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_react4.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_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.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_react4.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_react4.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
-+ (0, import_react4.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
-+ );
-+}
-+function WaveIcon({ size = 16 }) {
-+ return (0, import_react4.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_react4.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
-+ );
-+}
-+function ShareIcon() {
-+ return (0, import_react4.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_react4.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_react4.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react4.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
-+}
-+var ChevronLeftIcon = () => (0, import_react4.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_react4.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" }));
-+var ChevronRightIcon = () => (0, import_react4.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_react4.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }));
-+var lineIcon = (paths) => function LineIcon() {
-+ return (0, import_react4.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_react4.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_react4.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_react4.createElement)("path", { d: CLOUD }),
-+ done ? (0, import_react4.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react4.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react4.createElement)("path", { d: "M10 9.75v7" }), (0, import_react4.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" }))
-+ );
-+}
-+var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]);
-+var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]);
-+var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]);
-+var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]);
-+var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]);
-+var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]);
-+var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]);
-+var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]);
-+var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]);
-+var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]);
-+var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]);
-+var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]);
-+var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]);
-+var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]);
-+var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
-+function PinMarkIcon() {
-+ return (0, import_react4.createElement)(
-+ "svg",
-+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
-+ (0, import_react4.createElement)(
-+ "g",
-+ { transform: "rotate(45 10 10)" },
-+ (0, import_react4.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_react4.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
-+ )
-+ );
-+}
-+var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]);
-+var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]);
-+var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]);
-+var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]);
-+var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]);
-+var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
-+var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
-+var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
-+var 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 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 };
-+function FlyBotMark({ size = 24 }) {
-+ return (0, import_react4.createElement)(
-+ "svg",
-+ { className: "bt-flybot-mark", width: size, height: size, viewBox: "0 0 512 512", "aria-hidden": true },
-+ (0, import_react4.createElement)(
-+ "g",
-+ { className: "bt-flybot-wing" },
-+ (0, import_react4.createElement)("path", { d: FLYBOT_WING }),
-+ (0, import_react4.createElement)("path", { d: FLYBOT_WING_LOW })
-+ ),
-+ (0, import_react4.createElement)(
-+ "g",
-+ { className: "bt-flybot-wing", transform: "translate(512 0) scale(-1 1)" },
-+ (0, import_react4.createElement)("path", { d: FLYBOT_WING }),
-+ (0, import_react4.createElement)("path", { d: FLYBOT_WING_LOW })
-+ ),
-+ (0, import_react4.createElement)("path", { className: "bt-flybot-body", d: FLYBOT_BODY }),
-+ (0, import_react4.createElement)("rect", { className: "bt-flybot-eye", x: 196, y: 222, width: 34, height: 58, rx: 17 }),
-+ (0, import_react4.createElement)("rect", { className: "bt-flybot-eye", x: 282, y: 222, width: 34, height: 58, rx: 17 }),
-+ (0, import_react4.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }),
-+ (0, import_react4.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 })
-+ );
-+}
+ }
--.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/questions.js
+ // src/client/icons.js
+-var import_react3 = require("react");
+var import_react5 = 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_react5.useRef)(onPick);
-+ pick2.current = onPick;
-+ (0, import_react5.useEffect)(() => {
-+ if (count === 0) return void 0;
-+ const onKey = (event) => {
-+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return;
-+ if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return;
-+ if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return;
-+ const index = event.key.toUpperCase().charCodeAt(0) - 65;
-+ if (index >= 0 && index < count) {
-+ event.preventDefault();
-+ pick2.current(index);
-+ } else if (index === count && ownRef.current) {
-+ event.preventDefault();
-+ ownRef.current.focus();
-+ }
-+ };
-+ document.addEventListener("keydown", onKey, true);
-+ return () => document.removeEventListener("keydown", onKey, true);
-+ }, [count, ownRef]);
-+}
-+function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) {
-+ const [own, setOwn] = (0, import_react5.useState)("");
-+ const [picked, setPicked] = (0, import_react5.useState)([]);
-+ const ownRef = (0, import_react5.useRef)(null);
-+ const titleId = (0, import_react5.useId)();
-+ const options = optionsOf(question);
-+ const multi = question.multiSelect === true;
-+ const custom = own.trim();
-+ const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== "";
-+ const choose = (label) => {
-+ if (multi) {
-+ setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
-+ return;
-+ }
-+ onAnswer({ selected: [label] });
-+ };
-+ const submit = () => {
-+ if (!canSubmit) return;
-+ onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} });
-+ };
-+ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef);
-+ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0;
-+ const field = ownAnswer ? (0, import_react5.createElement)("textarea", {
-+ ref: ownRef,
-+ rows: 1,
-+ value: own,
-+ autoComplete: "off",
-+ spellCheck: false,
-+ className: "bt-q-input",
-+ "aria-label": t("Your own answer"),
-+ placeholder: t("Type your own answer"),
-+ "aria-keyshortcuts": ownLetter?.toLowerCase(),
-+ onChange: (event) => setOwn(event.target.value),
-+ onKeyDown: (event) => {
-+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
-+ event.preventDefault();
-+ submit();
-+ }
-+ }) : null;
-+ const rows = options.map((option, optionIndex) => {
-+ const selected = multi && picked.includes(option.label);
-+ return (0, import_react5.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_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
-+ (0, import_react5.createElement)(
-+ "span",
-+ { className: "bt-q-body" },
-+ (0, import_react5.createElement)("span", { className: "bt-q-label" }, option.label),
-+ option.description ? (0, import_react5.createElement)("span", { className: "bt-q-desc" }, option.description) : null
-+ ),
-+ selected && !raised ? (0, import_react5.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react5.createElement)(CheckIcon)) : null
-+ );
-+ });
-+ if (raised && field) {
-+ rows.push((0, import_react5.createElement)(
-+ "label",
-+ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 },
-+ ownLetter ? (0, import_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
-+ (0, import_react5.createElement)("span", { className: "bt-q-body" }, field)
-+ ));
-+ }
-+ let foot = null;
-+ if (raised && multi) {
-+ foot = (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q-foot" },
-+ (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
-+ (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
-+ );
-+ } else if ((raised || multi) && canSubmit) {
-+ foot = (0, import_react5.createElement)("div", { className: "bt-q-foot" }, (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
-+ }
+ function PlusIcon() {
+- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++ return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
+ }
+ function SearchIcon() {
+- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
+ }
+ function PeopleIcon() {
+- return (0, import_react3.createElement)(
+ return (0, import_react5.createElement)(
-+ "form",
-+ {
-+ className: raised ? "bt-q bt-q-raised" : "bt-q",
-+ "aria-labelledby": titleId,
-+ onSubmit: (event) => {
-+ event.preventDefault();
-+ submit();
-+ }
-+ },
-+ (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q-head" },
-+ (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q-text" },
-+ (0, import_react5.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
-+ question.detail ? (0, import_react5.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
-+ raised && multi ? (0, import_react5.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
-+ ),
-+ (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react5.createElement)(CloseIcon))
-+ ),
-+ rows.length > 0 ? (0, import_react5.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
-+ field && !raised ? (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q-custom" },
-+ (0, import_react5.createElement)("label", { className: "bt-q-field" }, field),
-+ !multi && canSubmit ? (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
-+ ) : null,
-+ foot
-+ );
-+}
-+function SettledQuestion({ question, answer }) {
-+ const options = optionsOf(question);
-+ const prompt = question?.question ?? "";
-+ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []];
-+ if (chosen.length === 0) {
-+ return (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt },
-+ (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q-head" },
-+ (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)("p", { className: "bt-q-title" }, prompt)),
-+ (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
-+ )
-+ );
-+ }
+ "svg",
+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true },
+- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
+- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
+- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
++ (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
++ (0, import_react5.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
++ (0, import_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
+ );
+ }
+ function ArrowUpIcon() {
+- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
++ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
+ }
+ function MicIcon({ size = 16 }) {
+- return (0, import_react3.createElement)(
+ return (0, import_react5.createElement)(
-+ "div",
-+ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
-+ prompt ? (0, import_react5.createElement)("div", { className: "bt-q-head" }, (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
-+ (0, import_react5.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_react5.createElement)(
-+ "div",
-+ { key: `${label}-${index}`, className: "bt-q-opt" },
-+ optionIndex >= 0 ? (0, import_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
-+ (0, import_react5.createElement)("span", { className: "bt-q-body" }, (0, import_react5.createElement)("span", { className: "bt-q-label" }, label)),
-+ (0, import_react5.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react5.createElement)(CheckIcon))
-+ );
-+ }))
-+ );
-+}
-+function QuestionCard({ matched }) {
-+ const pending = matched;
-+ const questions = pending.questions ?? [];
-+ const [index, setIndex] = (0, import_react5.useState)(0);
-+ const [answers, setAnswers] = (0, import_react5.useState)([]);
-+ const question = questions[index];
-+ if (question === void 0) return null;
-+ const onAnswer = (answer) => {
-+ const next = [...answers, { id: question.id, ...answer }];
-+ if (index + 1 < questions.length) {
-+ setAnswers(next);
-+ setIndex(index + 1);
-+ return;
-+ }
-+ pending.answer({ answers: next });
-+ };
-+ return (0, import_react5.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+ "svg",
+ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
+- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
+- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
++ (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
++ (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
+ );
}
--@keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
--@keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
--[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
--[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important}
--[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
--[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
--[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
--[data-composer-card]>[class*="_row"]{display:contents!important}
--[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0}
--[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important}
--[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0}
--[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%}
--[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
--[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
--[data-composer-placeholder]{font-size:0!important;line-height:0!important}
--[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
--/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
--[data-composer-card] button[class*="_add"]{display:none!important}
--.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
--.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
--.bt-plus:active{scale:.9}
--.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}}
--.bt-plus[aria-expanded=true] svg{rotate:45deg}
--.bt-plus-menu{min-width:180px}
--[data-composer-card] [class*="_tools"]{gap:0!important}
--[data-composer-card] [class*="_modes"]:empty{display:none!important}
--[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important}
--[data-composer-card] [class*="_trailing"]{gap:8px}
--[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px}
--[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9}
--[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9}
--@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}}
--[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px}
--[data-composer-card] button[class*="_primary"]:disabled{display:none!important}
--[data-composer-dock]{display:none!important}
--.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}}
--.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)}
--.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)}
--.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite}
--@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}}
--.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
--.bt-voice:hover{opacity:.9}
--.bt-mic:active,.bt-voice:active{scale:.9}
--[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
--body[data-bt-room] .bt-voice{display:none!important}
--body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)}
--.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out}
--[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2}
--.bt-q{order:1}
--div:has(> nav [class*="_marks"]){display:none!important}
--/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies)
-- takes its surface and its text from the one --bt-user pair: a skin that repaints
-- the host's bubble surface alone would otherwise leave the text unreadable. */
--[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important}
--[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit}
--/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */
--[class*="_userRow"] [class*="_actions"]{display:none!important}
--[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start}
--[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none}
--[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible}
--[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none}
--[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)}
--[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)}
--[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px}
--.bt-q-inline{display:flex;margin:4px 0 6px}
--[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important}
--[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important}
--.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0}
--.bt-q-dock{width:100%;display:flex;justify-content:center}
--.bt-q-dock .bt-q{margin:0 auto 8px}
--[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important}
--[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important}
--[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}}
--[class*="_toBottomSlot"]>button:active{scale:.9}
--[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important}
--[class*="_toBottomSlot"]>button svg{display:none}
--[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat}
--@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}}
--[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important}
--.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out}
--@keyframes bt-news-in{from{translate:0 8px;scale:.9}}
--.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in}
--@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}}
--.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none}
--.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px}
--.bt-news-ico svg{width:16px;height:16px}
--.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
--.bt-news-x svg{width:12px;height:12px}
--.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)}
--@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}}
--.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
--.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)}
--.bt-open-new{display:flex;flex-direction:column}
--.bt-open-new .bt-new-sep{margin:18px 0 0}
--.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px}
--.bt-earlier{display:flex;flex-direction:column;gap:6px}
--.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
--.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
--.bt-urow{display:flex;justify-content:flex-end}
--.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
--.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
--.bt-ububble p{margin:0;line-height:22px}
--.bt-uimages{display:block;font-size:12px;opacity:.7}
--.bt-earlier .bt-event{margin:4px 0}
--.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)}
--.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)}
--`;
--function installStyles(text, tag) {
-- const style = document.createElement("style");
-- style.dataset.dshBot = tag;
-- style.textContent = text;
-- document.head.appendChild(style);
-- return style;
-+function PendingQuestion({ sessionId, question, actions, inline }) {
-+ const [answered, setAnswered] = (0, import_react5.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);
-+ };
+ function WaveIcon({ size = 16 }) {
+- return (0, import_react3.createElement)(
+ return (0, import_react5.createElement)(
-+ "div",
-+ { className: inline ? "bt-q-inline" : "bt-q-dock" },
-+ (0, import_react5.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_react5.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_react5.createElement)("div", { className: "bt-urow" }, (0, import_react5.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
-+ }
-+ const answers = Array.isArray(data.answers) ? data.answers : [];
-+ return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react5.createElement)(SettledQuestion, {
-+ key: question.id,
-+ question,
-+ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
-+ })));
-+}
-+
-+// src/client/secret-card.js
-+var import_react6 = require("react");
-+var MIN_VALUE = 4;
-+var NONE = {};
-+var KeyIcon = () => (0, import_react6.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_react6.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
-+ (0, import_react6.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
-+);
-+var LockIcon = () => (0, import_react6.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_react6.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
-+ (0, import_react6.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
-+);
-+var WarnIcon = () => (0, import_react6.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_react6.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
-+ (0, import_react6.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 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_react6.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 my Bots")], ["bot", t("Only {name}", { name: bot.name })]];
-+ return (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
-+ (0, import_react6.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
-+ options.map(([id, text]) => (0, import_react6.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_react6.createElement)(
-+ "div",
-+ { className: "bt-q-head bt-secret-head" },
-+ (0, import_react6.createElement)("span", { className: "bt-secret-who" }, (0, import_react6.createElement)(BotMark, { bot, size: 28 }), (0, import_react6.createElement)("span", { className: "bt-secret-lock" }, (0, import_react6.createElement)(LockIcon))),
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-q-text" },
-+ (0, import_react6.createElement)("p", { className: "bt-q-title" }, title),
-+ (0, import_react6.createElement)("div", { className: "bt-q-detail" }, (0, import_react6.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
-+ ),
-+ onClose ? (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react6.createElement)(CloseIcon)) : null
-+ );
-+}
-+function SecretCard({ bot, request, roster, actions }) {
-+ const [value, setValue] = (0, import_react6.useState)("");
-+ const [scope, setScope] = (0, import_react6.useState)("all");
-+ const [replacing, setReplacing] = (0, import_react6.useState)(false);
-+ const [busy, setBusy] = (0, import_react6.useState)(false);
-+ const [error, setError] = (0, import_react6.useState)("");
-+ const noteId = (0, import_react6.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_react6.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react6.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null;
-+ return (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind },
-+ (0, import_react6.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_react6.createElement)("div", { className: "bt-secret-saved" }, (0, import_react6.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null,
-+ filling ? (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-body" },
-+ (0, import_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })),
-+ request.kind === "fill" ? (0, import_react6.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null
-+ ) : null,
-+ (0, import_react6.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_react6.createElement)("code", null, `$DSH_SECRET_${request.name}`),
-+ ".",
-+ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : ""
-+ ),
-+ warn,
-+ error ? (0, import_react6.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null,
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-q-foot" },
-+ request.kind === "allow" && !replacing ? (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
-+ (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
-+ filling ? (0, import_react6.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react6.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_react6.createElement)(
-+ "div",
-+ { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status },
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-q-head" },
-+ (0, import_react6.createElement)("span", { className: "bt-secret-who" }, (0, import_react6.createElement)(BotMark, { bot, size: 22 })),
-+ (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)(
-+ "p",
-+ { className: "bt-q-title" },
-+ text,
-+ canceled ? null : (0, import_react6.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
-+ )),
-+ canceled ? (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react6.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react6.createElement)(CheckIcon))
-+ )
-+ );
+ "svg",
+ { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
+- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
++ [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
+ );
}
--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 {
-+function RequestView({ bot, request, roster, actions }) {
-+ return request.status === "pending" ? (0, import_react6.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react6.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_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(RequestView, { bot, request, roster, actions }));
-+ if (done && /already saved/.test(contentText(content))) {
-+ return (0, import_react6.createElement)("div", { className: "bt-event" }, (0, import_react6.createElement)("span", { className: "bt-secret-event" }, (0, import_react6.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
- }
-- return names;
-+ return null;
+ function ShareIcon() {
+- return (0, import_react3.createElement)(
++ return (0, import_react5.createElement)(
+ "svg",
+ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
++ (0, import_react5.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
+ );
}
--function 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 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_react6.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react6.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
+ function CloseIcon() {
+- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++ return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
}
--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);
-+var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some";
-+function ScopeEditor({ secret, roster, onSave }) {
-+ const [mode, setMode] = (0, import_react6.useState)(scopeMode(secret.scope));
-+ const [picked, setPicked] = (0, import_react6.useState)(secret.scope === "all" ? [] : secret.scope);
-+ 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_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-edit-block" },
-+ (0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
-+ (0, import_react6.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_react6.createElement)(
-+ "label",
-+ { key: id, className: "bt-secret-radio" },
-+ (0, import_react6.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
-+ text
-+ ))
+-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)(
++ 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 }))
++ 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)(
++ 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" }))
++ (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 +4787,14 @@
+ var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]);
+ var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
+ function PinMarkIcon() {
+- return (0, import_react3.createElement)(
++ return (0, import_react5.createElement)(
+ "svg",
+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
+- (0, import_react3.createElement)(
++ (0, import_react5.createElement)(
+ "g",
+ { transform: "rotate(45 10 10)" },
+- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
+- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
++ (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
++ (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
+ )
+ );
+ }
+@@ -3112,21 +4806,48 @@
+ var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
+ var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
+ var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
++var 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 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 };
++function FlyBotMark({ size = 24 }) {
++ return (0, import_react5.createElement)(
++ "svg",
++ { className: "bt-flybot-mark", width: size, height: size, viewBox: "0 0 512 512", "aria-hidden": true },
++ (0, import_react5.createElement)(
++ "g",
++ { className: "bt-flybot-wing" },
++ (0, import_react5.createElement)("path", { d: FLYBOT_WING }),
++ (0, import_react5.createElement)("path", { d: FLYBOT_WING_LOW })
+ ),
-+ mode === "some" ? (0, import_react6.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react6.createElement)("button", {
-+ key: bot.id,
-+ type: "button",
-+ className: "bt-secret-bot",
-+ "aria-pressed": picked.includes(bot.id),
-+ onClick: () => toggle(bot.id)
-+ }, (0, import_react6.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react6.createElement)(CheckIcon) : null))) : null,
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-row-actions" },
-+ (0, import_react6.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_react6.useState)("");
-+ const [replaced, setReplaced] = (0, import_react6.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);
- }
-- 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 edit = (next) => {
-+ setValue(next);
-+ setReplaced(false);
- };
-- const observer = new MutationObserver(schedule);
-- observer.observe(document.head, { childList: true });
-- document.addEventListener("load", onLoad, true);
-- return () => {
-- observer.disconnect();
-- document.removeEventListener("load", onLoad, true);
-- clearTimeout(timer);
-- style.remove();
-+ return (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-edit-block" },
-+ (0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-inline" },
-+ (0, import_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
-+ (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
++ (0, import_react5.createElement)(
++ "g",
++ { className: "bt-flybot-wing", transform: "translate(512 0) scale(-1 1)" },
++ (0, import_react5.createElement)("path", { d: FLYBOT_WING }),
++ (0, import_react5.createElement)("path", { d: FLYBOT_WING_LOW })
+ ),
-+ value !== "" && !long ? (0, import_react6.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react6.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react6.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null
++ (0, import_react5.createElement)("path", { className: "bt-flybot-body", d: FLYBOT_BODY }),
++ (0, import_react5.createElement)("rect", { className: "bt-flybot-eye", x: 196, y: 222, width: 34, height: 58, rx: 17 }),
++ (0, import_react5.createElement)("rect", { className: "bt-flybot-eye", x: 282, y: 222, width: 34, height: 58, rx: 17 }),
++ (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }),
++ (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 })
+ );
+}
-+function SecretRow({ secret, roster, actions, run, open, onToggle }) {
-+ const [confirm, setConfirm] = (0, import_react6.useState)(false);
+
+ // src/client/questions.js
+-var import_react4 = require("react");
++var import_react6 = require("react");
+ var keyLetter = (index) => String.fromCharCode(65 + index);
+ var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
+ var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
+ function useOptionKeys(count, onPick, ownRef) {
+- const pick2 = (0, import_react4.useRef)(onPick);
++ const pick2 = (0, import_react6.useRef)(onPick);
+ pick2.current = onPick;
+- (0, import_react4.useEffect)(() => {
+ (0, import_react6.useEffect)(() => {
-+ if (!open) setConfirm(false);
-+ }, [open]);
-+ const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you");
-+ const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean);
-+ const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · ");
+ if (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 +4867,10 @@
+ }, [count, ownRef]);
+ }
+ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) {
+- const [own, setOwn] = (0, import_react4.useState)("");
+- const [picked, setPicked] = (0, import_react4.useState)([]);
+- const ownRef = (0, import_react4.useRef)(null);
+- const titleId = (0, import_react4.useId)();
++ 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 +4888,7 @@
+ };
+ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef);
+ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0;
+- const field = ownAnswer ? (0, import_react4.createElement)("textarea", {
++ const field = ownAnswer ? (0, import_react6.createElement)("textarea", {
+ ref: ownRef,
+ rows: 1,
+ value: own,
+@@ -3186,7 +4907,7 @@
+ }) : null;
+ const rows = options.map((option, optionIndex) => {
+ const selected = multi && picked.includes(option.label);
+- return (0, import_react4.createElement)(
++ return (0, import_react6.createElement)(
+ "button",
+ {
+ key: option.label,
+@@ -3196,36 +4917,36 @@
+ "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(),
+ onClick: () => choose(option.label)
+ },
+- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
+- (0, import_react4.createElement)(
++ (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
++ (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)(
++ 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)
++ 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)(
++ 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"))
++ (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")));
++ 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)(
-+ "div",
-+ { className: "bt-secret-item", "data-open": open ? "" : void 0 },
+ "form",
+ {
+ className: raised ? "bt-q bt-q-raised" : "bt-q",
+@@ -3235,24 +4956,24 @@
+ submit();
+ }
+ },
+- (0, import_react4.createElement)(
+ (0, import_react6.createElement)(
-+ "button",
-+ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
-+ (0, import_react6.createElement)(
-+ "span",
-+ { className: "bt-set-copy" },
-+ (0, import_react6.createElement)("span", { className: "bt-secret-title" }, (0, import_react6.createElement)(KeyIcon), (0, import_react6.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react6.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
-+ (0, import_react6.createElement)("span", { className: "bt-set-hint" }, meta)
-+ ),
+ "div",
+ { className: "bt-q-head" },
+- (0, import_react4.createElement)(
+ (0, import_react6.createElement)(
-+ "span",
-+ { className: "bt-set-control" },
-+ secret.scope === "all" ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react6.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react6.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
-+ )
-+ ),
-+ open ? (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-edit" },
-+ (0, import_react6.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
-+ (0, import_react6.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
+ "div",
+ { className: "bt-q-text" },
+- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
+- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
+- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
++ (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
++ question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
++ raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
+ ),
+- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon))
++ (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon))
+ ),
+- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
+- field && !raised ? (0, import_react4.createElement)(
++ rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
++ field && !raised ? (0, import_react6.createElement)(
+ "div",
+ { className: "bt-q-custom" },
+- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field),
+- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
++ (0, import_react6.createElement)("label", { className: "bt-q-field" }, field),
++ !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
+ ) : null,
+ foot
+ );
+@@ -3262,29 +4983,29 @@
+ const prompt = question?.question ?? "";
+ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []];
+ if (chosen.length === 0) {
+- return (0, import_react4.createElement)(
++ return (0, import_react6.createElement)(
+ "div",
+ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt },
+- (0, import_react4.createElement)(
+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-row-actions" },
-+ confirm ? [
-+ (0, import_react6.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
-+ (0, import_react6.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
-+ (0, import_react6.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_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react6.createElement)(TrashIcon), t("Delete"))
-+ )
-+ ) : null
-+ );
-+}
-+function AddSecret({ actions, run, onDone }) {
-+ const [name, setName] = (0, import_react6.useState)("");
-+ const [purpose, setPurpose] = (0, import_react6.useState)("");
-+ const [value, setValue] = (0, import_react6.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();
-+ }
-+ });
+ "div",
+ { className: "bt-q-head" },
+- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)),
+- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
++ (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)),
++ (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
+ )
+ );
+ }
+- return (0, import_react4.createElement)(
++ return (0, import_react6.createElement)(
+ "div",
+ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
+- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
+- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
++ prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
++ (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
+ const optionIndex = options.findIndex((option) => option.label === label);
+- return (0, import_react4.createElement)(
++ 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))
++ 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 +5013,8 @@
+ function QuestionCard({ matched }) {
+ const pending = matched;
+ const questions = pending.questions ?? [];
+- const [index, setIndex] = (0, import_react4.useState)(0);
+- const [answers, setAnswers] = (0, import_react4.useState)([]);
++ 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 +5026,10 @@
+ }
+ pending.answer({ answers: next });
+ };
+- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
++ return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+ }
+ function PendingQuestion({ sessionId, question, actions, inline }) {
+- const [answered, setAnswered] = (0, import_react4.useState)(null);
++ const [answered, setAnswered] = (0, import_react6.useState)(null);
+ if (answered === question.id) return null;
+ const onAnswer = (answer) => {
+ setAnswered(question.id);
+@@ -3318,10 +5039,10 @@
+ setAnswered(question.id);
+ void actions.dismissQuestion(sessionId);
};
+- return (0, import_react4.createElement)(
+ return (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-set-row bt-set-paste bt-secret-add" },
-+ (0, import_react6.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_react6.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_react6.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react6.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })),
-+ (0, import_react6.createElement)(
-+ "div",
-+ { className: "bt-secret-row-actions" },
-+ (0, import_react6.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")),
-+ (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
-+ (0, import_react6.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_react6.useState)(null);
-+ const [adding, setAdding] = (0, import_react6.useState)(false);
-+ const secrets = roster.secrets ?? [];
-+ return [
-+ (0, import_react6.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_react6.createElement)("code", null, "$DSH_SECRET_NAME"),
-+ t(" in their shell, and every value is replaced by "),
-+ (0, import_react6.createElement)("code", null, "[secret:NAME]"),
-+ t(" before anything goes to a model.")
-+ ),
-+ (0, import_react6.createElement)(
-+ Section,
-+ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
-+ secrets.length === 0 && !adding ? (0, import_react6.createElement)("div", { className: "bt-set-row" }, (0, import_react6.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_react6.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
-+ adding ? (0, import_react6.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
-+ ),
-+ adding ? null : (0, import_react6.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key")))
-+ ];
+ "div",
+ { className: inline ? "bt-q-inline" : "bt-q-dock" },
+- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
++ (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
+ );
+ }
+ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
+@@ -3329,16 +5050,16 @@
+ const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true;
+ const loading = useSession?.(historyLoading) === true;
+ if (question === void 0 || question.callId && (drawn || loading)) return null;
+- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
++ 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;
++ return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
+ }
+ const answers = Array.isArray(data.answers) ? data.answers : [];
+- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
++ return (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 +5067,37 @@
+ }
+
+ // src/client/secret-card.js
+-var import_react5 = require("react");
++var import_react7 = require("react");
+ var MIN_VALUE = 4;
+ var NONE = {};
+-var KeyIcon = () => (0, import_react5.createElement)(
++var KeyIcon = () => (0, import_react7.createElement)(
+ "svg",
+ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
+- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
++ (0, import_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
++ (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
+ );
+-var LockIcon = () => (0, import_react5.createElement)(
++var LockIcon = () => (0, import_react7.createElement)(
+ "svg",
+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
+- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
++ (0, import_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
++ (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
+ );
+-var WarnIcon = () => (0, import_react5.createElement)(
++var WarnIcon = () => (0, import_react7.createElement)(
+ "svg",
+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
+- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
++ (0, import_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
++ (0, import_react7.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
+ );
+ var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
+ var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
+ var scopeLabel = (scope, roster) => {
+- if (scope === "all") return t("All Bots");
++ if (scope === "all") return t("All my Bots");
+ const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
+ return names.length === 0 ? t("No Bot") : names.join(t(" and "));
+ };
+ var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
+ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
+- return (0, import_react5.createElement)("input", {
++ return (0, import_react7.createElement)("input", {
+ className: "bt-secret-input",
+ type: MASK_BY_CSS ? "text" : "password",
+ "data-masked": MASK_BY_CSS ? "" : void 0,
+@@ -3401,12 +5122,12 @@
+ });
+ }
+ function ScopeSwitch({ value, onChange, bot }) {
+- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
+- return (0, import_react5.createElement)(
++ const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]];
++ return (0, import_react7.createElement)(
+ "div",
+ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
+- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
+- options.map(([id, text]) => (0, import_react5.createElement)("button", {
++ (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
++ options.map(([id, text]) => (0, import_react7.createElement)("button", {
+ key: id,
+ type: "button",
+ role: "radio",
+@@ -3417,26 +5138,26 @@
+ );
+ }
+ function SecretHead({ bot, title, detail, name, onClose }) {
+- return (0, import_react5.createElement)(
++ return (0, import_react7.createElement)(
+ "div",
+ { className: "bt-q-head bt-secret-head" },
+- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))),
+- (0, import_react5.createElement)(
++ (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))),
++ (0, import_react7.createElement)(
+ "div",
+ { className: "bt-q-text" },
+- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title),
+- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
++ (0, import_react7.createElement)("p", { className: "bt-q-title" }, title),
++ (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
+ ),
+- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null
++ onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null
+ );
+ }
+ function SecretCard({ bot, request, roster, actions }) {
+- const [value, setValue] = (0, import_react5.useState)("");
+- const [scope, setScope] = (0, import_react5.useState)("all");
+- const [replacing, setReplacing] = (0, import_react5.useState)(false);
+- const [busy, setBusy] = (0, import_react5.useState)(false);
+- const [error, setError] = (0, import_react5.useState)("");
+- const noteId = (0, import_react5.useId)();
++ const [value, setValue] = (0, import_react7.useState)("");
++ const [scope, setScope] = (0, import_react7.useState)("all");
++ const [replacing, setReplacing] = (0, import_react7.useState)(false);
++ const [busy, setBusy] = (0, import_react7.useState)(false);
++ const [error, setError] = (0, import_react7.useState)("");
++ const noteId = (0, import_react7.useId)();
+ const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name);
+ const run = (promise) => {
+ setBusy(true);
+@@ -3452,73 +5173,73 @@
+ if (!busy) void run(actions.secretAllow(request.id));
+ };
+ const filling = request.kind === "fill" || replacing;
+- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null;
+- return (0, import_react5.createElement)(
++ const 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, {
++ (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)(
++ 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
++ (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)(
++ (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}`),
++ (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)(
++ 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"))
++ 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)(
++ 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)(
++ (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)(
++ (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)}`)
++ 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 });
++ 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 }));
++ 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") })));
++ 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 +5252,46 @@
+ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0);
+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry));
+ if (waiting.length === 0) return null;
+- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
++ 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);
++ 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 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)(
++ (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)(
++ [["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) }),
++ (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", {
++ 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)(
++ }, (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"))
++ (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);
++ 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 +5306,66 @@
+ setValue(next);
+ setReplaced(false);
+ };
+- return (0, import_react5.createElement)(
++ 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)(
++ (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"))
++ (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)(() => {
++ 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)(
++ return (0, import_react7.createElement)(
+ "div",
+ { className: "bt-secret-item", "data-open": open ? "" : void 0 },
+- (0, import_react5.createElement)(
++ (0, import_react7.createElement)(
+ "button",
+ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
+- (0, import_react5.createElement)(
++ (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)
++ (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 })))
++ 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)(
++ (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"))
++ (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)("");
++ 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 +5376,43 @@
+ }
+ });
+ };
+- return (0, import_react5.createElement)(
++ 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)(
++ (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"))
++ (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"))
+ )
+ );
}
-+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 SecretsPage({ roster, actions, run, Section }) {
+- const [open, setOpen] = (0, import_react5.useState)(null);
+- const [adding, setAdding] = (0, import_react5.useState)(false);
++ const [open, setOpen] = (0, import_react7.useState)(null);
++ const [adding, setAdding] = (0, import_react7.useState)(false);
+ const secrets = roster.secrets ?? [];
+ return [
+- (0, import_react5.createElement)(
++ (0, import_react7.createElement)(
+ "p",
+ { key: "about", className: "bt-secret-about" },
+ t("Keys your Bots asked for. Bots never see the values: they use them as "),
+- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"),
++ (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"),
+ t(" in their shell, and every value is replaced by "),
+- (0, import_react5.createElement)("code", null, "[secret:NAME]"),
++ (0, import_react7.createElement)("code", null, "[secret:NAME]"),
+ t(" before anything goes to a model.")
+ ),
+- (0, import_react5.createElement)(
++ (0, import_react7.createElement)(
+ Section,
+ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
+- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
+- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
+- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
++ 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 +5479,144 @@
+ @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
+ `;
--// 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);
+// src/client/face-seats.js
+function faceSeats(room2, roster, { left = false } = {}) {
+ const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human");
@@ -5700,45 +3951,8 @@ diff --git a/client.js b/client.js
+ person: local ? seatOwnerName(p.ownerName, p.handle) || cleanPeerName(roster.self?.displayName, "") : ownerNameOf(p),
+ bot: local ? local.name : remoteBotName(p, "Bot"),
+ mine: Boolean(local)
- };
-- }, [enabled]);
--}
--var FIDGETS = ["hop", "sway", "stretch", "look"];
--function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) {
-- const look = given ?? lookOf(bot);
-- const ref = (0, import_react2.useRef)(null);
-- const maskId = (0, import_react2.useRef)("");
-- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`;
-- const [poked, setPoked] = (0, import_react2.useState)(false);
-- useGaze(ref, gaze);
-- const common = {
-- ref,
-- className: poked ? "bt-mark bt-mark-poke" : "bt-mark",
-- "data-shape": look.image ? "image" : shapeId(look.shape),
-- "data-state": STATES.includes(state) ? state : "idle",
-- "data-live": live ? "" : void 0,
-- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0,
-- "data-gaze": gaze ? "" : void 0,
-- "data-poke": pokeable ? "" : void 0,
-- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} },
-- role: label ? "img" : void 0,
-- "aria-label": label,
-- "aria-hidden": label ? void 0 : true,
-- onClick: pokeable ? () => setPoked(true) : void 0,
-- onAnimationEnd: poked ? (event) => {
-- if (/^bt-poke/.test(event.animationName)) setPoked(false);
-- } : void 0
- };
-- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false }));
-- const box = shapeOf(look.shape).size;
-- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } }));
--}
--function StarIcon({ size = 8 }) {
-- return (0, import_react2.createElement)(
-- "svg",
-- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true },
-- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" })
-- );
++ };
++ };
+ const seats = records.filter((p) => p.leftSeq === void 0).map(seat);
+ for (const id of room2.members ?? []) {
+ const bot = roster.byId[id];
@@ -5779,23 +3993,16 @@ diff --git a/client.js b/client.js
+ people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] });
+ }
+ return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)];
- }
--function 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 facePersonLabel(person) {
+ return person.mine ? t("{name} (you)", { name: person.name }) : person.name;
- }
--function MainStar() {
-- return (0, import_react2.createElement)(
-- "svg",
-- { viewBox: "0 0 20 20", "aria-hidden": true },
-- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" })
++}
+
+// src/client/held-reply.js
-+var import_react7 = require("react");
++var import_react8 = require("react");
+var call3 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
+var Boundary;
-+var boundary = () => Boundary ??= class HeldReplyBoundary extends import_react7.Component {
++var boundary = () => Boundary ??= class HeldReplyBoundary extends import_react8.Component {
+ constructor(props) {
+ super(props);
+ this.state = { failed: false };
@@ -5812,16 +4019,16 @@ diff --git a/client.js b/client.js
+};
+function useInlineEntry(policy) {
+ const slots = registry();
-+ const [, setVersion] = (0, import_react7.useState)(0);
-+ (0, import_react7.useEffect)(() => slots.subscribe(() => setVersion(slots.version())), [slots]);
++ const [, setVersion] = (0, import_react8.useState)(0);
++ (0, import_react8.useEffect)(() => slots.subscribe(() => setVersion(slots.version())), [slots]);
+ return slots.entries("flybot.approvals").find((entry) => entry.inline === true && entry.policy === policy);
+}
+function HeldReplyActions({ held, actions }) {
+ const custom = useInlineEntry(held?.by);
-+ const [busy, setBusy] = (0, import_react7.useState)(false);
-+ const [error, setError] = (0, import_react7.useState)("");
++ const [busy, setBusy] = (0, import_react8.useState)(false);
++ const [error, setError] = (0, import_react8.useState)("");
+ const waiting = isApprovalWaiting(held?.status);
-+ (0, import_react7.useEffect)(() => {
++ (0, import_react8.useEffect)(() => {
+ if (!waiting) return void 0;
+ claimApprovals(`held-reply:${held.id}`, [held.id]);
+ return () => claimApprovals(`held-reply:${held.id}`, []);
@@ -5840,556 +4047,297 @@ diff --git a/client.js b/client.js
+ };
+ if (!waiting) {
+ const failed = held.status === "failed";
-+ return (0, import_react7.createElement)(
++ return (0, import_react8.createElement)(
+ "div",
+ { className: "bt-held-note", "data-state": held.status, title: failed && held.error ? held.error : void 0 },
-+ (0, import_react7.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")),
++ (0, import_react8.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")),
+ // #968: the terminal answer comes from one predicate, not from `status === 'failed'` here and a
+ // different field elsewhere. A failure that can never succeed draws no button on any surface.
-+ failed && canRetry(held) ? (0, import_react7.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
-+ error ? (0, import_react7.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
++ failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
++ error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
+ );
+ }
-+ const plain = (0, import_react7.createElement)(
++ const plain = (0, import_react8.createElement)(
+ "div",
+ { className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") },
-+ (0, import_react7.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")),
-+ (0, import_react7.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
-+ (0, import_react7.createElement)(
++ (0, import_react8.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")),
++ (0, import_react8.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
++ (0, import_react8.createElement)(
+ "div",
+ { className: "bt-flybot-approval-actions" },
-+ (0, import_react7.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")),
-+ (0, import_react7.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve"))
++ (0, import_react8.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")),
++ (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve"))
+ ),
-+ error ? (0, import_react7.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
++ error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
+ );
-+ return (0, import_react7.createElement)(
++ return (0, import_react8.createElement)(
+ "div",
+ { className: "bt-held-actions", "data-flybot": "held-reply-actions" },
-+ custom ? (0, import_react7.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react7.createElement)(custom.component, { held, decide, call: call3, actions })) : plain
- );
- }
--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;
-+
-+// src/client/turn-reasoning.js
-+function turnReasoning(steps, current) {
-+ const members = steps.length ? steps : [current];
-+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
-+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
- }
--function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) {
-- const settled = useSettled(state);
-- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size });
-- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null;
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } },
-- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }),
-- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null,
-- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null,
-- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null,
-- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null
-+var 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();
++ custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain
++ );
++}
+
-+// src/client/cells.js
-+var import_react8 = 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_react8.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_react8.createElement)(
-+ "div",
-+ { key: index, className: "bt-msg" },
-+ (0, import_react8.createElement)(
-+ "div",
-+ { className: "bt-msg-line" },
-+ lead && last ? lead : null,
-+ (0, import_react8.createElement)(
-+ "div",
-+ { className: "bt-bubble" },
-+ (0, import_react8.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_react8.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_react8.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react8.createElement)("button", {
-+ key: emoji,
-+ type: "button",
-+ className: "bt-react",
-+ "aria-label": t("Remove reaction {emoji}", { emoji }),
-+ onClick: () => actions.toggleReaction(msgKey, index, emoji)
-+ }, emoji))) : null
-+ );
-+ })
+ // src/client/turn-reasoning.js
+ function turnReasoning(steps, current) {
+ const members = steps.length ? steps : [current];
+@@ -3767,7 +5628,7 @@
+ var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
+
+ // src/client/cells.js
+-var import_react6 = require("react");
++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 +5641,7 @@
+ actions.openSession(target);
+ };
+ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null;
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
+ texts2.map((text, index) => {
+@@ -3788,21 +5649,21 @@
+ const picked = key ? reactions?.[index] ?? [] : [];
+ const last = index === texts2.length - 1;
+ const live = streaming && last;
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { key: index, className: "bt-msg" },
+- (0, import_react6.createElement)(
++ (0, import_react9.createElement)(
+ "div",
+ { className: "bt-msg-line" },
+ lead && last ? lead : null,
+- (0, import_react6.createElement)(
++ (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 })
++ (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 +5674,22 @@
+ })
);
}
--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 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)?$/);
+ if (!match) return false;
-+ const who = match[1];
-+ for (const participant of room2.participants ?? []) {
-+ if (participant?.remote !== true || participant.leftSeq === void 0) continue;
-+ const name = String(participant.ownerName || participant.displayName || participant.name || "").trim();
-+ if (name && (who === name || who.endsWith(` (${name})`))) return true;
- }
-- const half = frame / 2;
-- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
-+ return false;
- }
--function RoomAvatar({ room: room2, roster, size = 36 }) {
-- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
-- const people = members.length === 1 ? [members[0], null] : members;
-- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
-- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
-- if (size <= 20 && people.length > 1) {
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
-- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
-- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
-- );
-- }
-- const seats = clusterSeats(Math.min(people.length, 4), size);
-- const overflow = people.length > 4 ? people.length - 3 : 0;
-- const gap = size / 14;
-- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap }));
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-cluster", style: { width: size, height: size } },
-- seats.map(([x, y, s], index) => {
-- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`);
-- const style = { left: x, top: y, width: s, height: s };
-- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" });
-- if (overflow > 0 && index === 3) {
-- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`);
-+function useDismiss(open, close, refs) {
-+ (0, import_react8.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();
- }
-- const bot = people[index];
-- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
-- })
-+ };
-+ 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_react8.useState)(null);
-+ const [place, setPlace] = (0, import_react8.useState)(null);
-+ const [copied, setCopied] = (0, import_react8.useState)(false);
-+ const tools = (0, import_react8.useRef)(null);
-+ const pop = (0, import_react8.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);
-+ };
-+ const item = (label, Icon, run) => (0, import_react8.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
-+ close();
-+ run();
-+ } }, (0, import_react8.createElement)(Icon), label);
-+ return (0, import_react8.createElement)(
-+ "div",
-+ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
-+ react ? (0, import_react8.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_react8.createElement)(SmileIcon)) : null,
-+ sessionId ? (0, import_react8.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react8.createElement)(ReplyIcon)) : null,
-+ (0, import_react8.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_react8.createElement)(CheckIcon) : (0, import_react8.createElement)(DotsIcon)),
-+ panel === "react" ? (0, import_react_dom.createPortal)((0, import_react8.createElement)(
-+ "div",
-+ { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place },
-+ REACTIONS.map((emoji) => (0, import_react8.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_react8.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
++ const who = match[1];
++ for (const participant of room2.participants ?? []) {
++ if (participant?.remote !== true || participant.leftSeq === void 0) continue;
++ const name = String(participant.ownerName || participant.displayName || participant.name || "").trim();
++ if (name && (who === name || who.endsWith(` (${name})`))) return true;
++ }
++ return false;
++}
+ function useDismiss(open, close, refs) {
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ if (!open) return void 0;
+ const onDown = (event) => {
+ if (refs.some((ref) => ref.current?.contains(event.target))) return;
+@@ -3836,11 +5710,11 @@
+ }, [open]);
+ }
+ function MessageTools({ text, name, sessionId, react, actions }) {
+- const [panel, setPanel] = (0, import_react6.useState)(null);
+- const [place, setPlace] = (0, import_react6.useState)(null);
+- const [copied, setCopied] = (0, import_react6.useState)(false);
+- const tools = (0, import_react6.useRef)(null);
+- const pop = (0, import_react6.useRef)(null);
++ const [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 +5739,25 @@
+ link.click();
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
+ };
+- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
++ const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
+ close();
+ run();
+- } }, (0, import_react6.createElement)(Icon), label);
+- return (0, import_react6.createElement)(
++ } }, (0, import_react9.createElement)(Icon), label);
++ return (0, import_react9.createElement)(
+ "div",
+ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
+- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null,
+- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null,
+- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)),
+- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
++ react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null,
++ sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, 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: () => {
++ 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)(
++ 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 +5766,12 @@
);
}
--
--// src/client/icons.js
--var import_react3 = require("react");
--function PlusIcon() {
-- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
--}
--function SearchIcon() {
-- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
--}
--function PeopleIcon() {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true },
-- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
-- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
-- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
-+function ThinkingRow({ text }) {
+ function ThinkingRow({ text }) {
+- return (0, import_react6.createElement)(
+ if (typeof text !== "string" || text.trim() === "") return null;
-+ return (0, import_react8.createElement)(
-+ "details",
-+ { className: "bt-think" },
-+ (0, import_react8.createElement)("summary", null, (0, import_react8.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), t("Thought it through")),
-+ (0, import_react8.createElement)("div", { className: "bt-think-text" }, text)
++ 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)
++ (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 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 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_react8.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 useFileMentions({ node, openFile, fileMentions, useTurnData }) {
+@@ -3904,19 +5779,33 @@
+ const location2 = node.location;
+ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0;
+ const seq = node.data.finalNode?.seq;
+- return (0, import_react6.useMemo)(() => {
++ 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]);
}
--function MicIcon({ size = 16 }) {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
-- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
-- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
-- );
+function useClosingText(node, useTurnData) {
+ const tail = useTurnData?.("turn-tail");
+ const seq = node.data.finalNode?.seq;
+ if (!tail?.closing || seq === void 0) return true;
+ return tail.closing.finalNode?.seq === seq;
- }
--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 friendLead(bot, texts2) {
+ if (bot?.remote === void 0 || texts2.length === 0) return void 0;
+ if (texts2.every((text) => /^Not sent:/.test(text.trim()))) return void 0;
-+ return (0, import_react8.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react8.createElement)(BotMark, { bot, size: 22 }));
- }
--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 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 });
++ 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 });
+ const held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn);
+ const closing = useClosingText(node, useTurnData);
-+ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
+ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
+- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
+ const person = typedBy(roster, sessionId, data.turn);
+ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((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;
-+ 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_react8.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react8.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_react8.createElement)(
-+ import_react8.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_react8.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_react8.createElement)(ThinkingRow, { text: reasoning }) : null,
-+ held && bubbles ? (0, import_react8.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
- );
- }
--function CloseIcon() {
-- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
--}
--var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" }));
--var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }));
--var lineIcon = (paths) => function LineIcon() {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d }))
-- );
--};
--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" }))
+ 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 +5813,14 @@
+ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : [];
+ if (texts2.length === 0 && !reasoning && relays.length === 0) return null;
+ const selfId = roster.byId[sessionId]?.id ?? sessionId;
+- return (0, import_react6.createElement)(
+- import_react6.Fragment,
++ const 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, {
++ 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 +5830,230 @@
+ whole: true
+ });
+ }),
+- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
+- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null
++ reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null,
++ held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
++ );
++}
+function HeldReply({ held, roster, sessionId, actions, closing, children }) {
+ const bot = roster.byId[sessionId];
+ const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to);
+ const toName = cleanPeerName(held.toOwner) || (chat ? friendBotOwner(chat) : ownerNameOf({ handle: held.to })) || held.to || t("a friend");
-+ return (0, import_react8.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-held-reply", "data-state": held.status, "data-held": isApprovalWaiting(held.status) ? "" : void 0 },
+ children,
-+ closing ? (0, import_react8.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
- );
- }
--var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]);
--var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]);
--var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]);
--var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]);
--var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]);
--var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]);
--var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]);
--var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]);
--var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]);
--var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]);
--var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]);
--var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]);
--var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]);
--var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]);
--var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
--function PinMarkIcon() {
-- return (0, import_react3.createElement)(
-- "svg",
-- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
-- (0, import_react3.createElement)(
-- "g",
-- { transform: "rotate(45 10 10)" },
-- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
-- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
-- )
++ closing ? (0, import_react9.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
++ );
++}
+function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) {
-+ const lead = (0, import_react8.createElement)("span", { className: "bt-lead", title: name }, (0, import_react8.createElement)(PersonMark, { name }));
-+ return (0, import_react8.createElement)(
++ const lead = (0, import_react9.createElement)("span", { className: "bt-lead", title: name }, (0, import_react9.createElement)(PersonMark, { name }));
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-group-msg bt-person-msg", "data-person": name },
-+ (0, import_react8.createElement)("span", { className: "bt-group-name" }, name),
-+ (0, import_react8.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead })
++ (0, import_react9.createElement)("span", { className: "bt-group-name" }, name),
++ (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead })
);
}
--var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]);
--var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]);
--var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]);
--var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]);
--var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]);
--var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
--var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
--var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
--var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]);
--var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]);
--var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]);
--var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]);
--var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon };
--
--// src/client/questions.js
--var import_react4 = require("react");
--var keyLetter = (index) => String.fromCharCode(65 + index);
--var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
--var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
--function useOptionKeys(count, onPick, ownRef) {
-- const pick2 = (0, import_react4.useRef)(onPick);
-- pick2.current = onPick;
-- (0, import_react4.useEffect)(() => {
-- if (count === 0) return void 0;
-- const onKey = (event) => {
-- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return;
-- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return;
-- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return;
-- const index = event.key.toUpperCase().charCodeAt(0) - 65;
-- if (index >= 0 && index < count) {
-- event.preventDefault();
-- pick2.current(index);
-- } else if (index === count && ownRef.current) {
-- event.preventDefault();
-- ownRef.current.focus();
-- }
-- };
-- document.addEventListener("keydown", onKey, true);
-- return () => document.removeEventListener("keydown", onKey, true);
-- }, [count, ownRef]);
--}
--function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) {
-- const [own, setOwn] = (0, import_react4.useState)("");
-- const [picked, setPicked] = (0, import_react4.useState)([]);
-- const ownRef = (0, import_react4.useRef)(null);
-- const titleId = (0, import_react4.useId)();
-- const options = optionsOf(question);
-- const multi = question.multiSelect === true;
-- const custom = own.trim();
-- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== "";
-- const choose = (label) => {
-- if (multi) {
-- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
-- return;
-- }
-- onAnswer({ selected: [label] });
-- };
-- const submit = () => {
-- if (!canSubmit) return;
-- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} });
-- };
-- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef);
-- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0;
-- const field = ownAnswer ? (0, import_react4.createElement)("textarea", {
-- ref: ownRef,
-- rows: 1,
-- value: own,
-- autoComplete: "off",
-- spellCheck: false,
-- className: "bt-q-input",
-- "aria-label": t("Your own answer"),
-- placeholder: t("Type your own answer"),
-- "aria-keyshortcuts": ownLetter?.toLowerCase(),
-- onChange: (event) => setOwn(event.target.value),
-- onKeyDown: (event) => {
-- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
-- event.preventDefault();
-- submit();
-- }
-- }) : null;
-- const rows = options.map((option, optionIndex) => {
-- const selected = multi && picked.includes(option.label);
-- return (0, import_react4.createElement)(
-- "button",
-- {
-- key: option.label,
-- type: "button",
-- className: "bt-q-opt",
-- "aria-pressed": multi ? selected : void 0,
-- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(),
-- onClick: () => choose(option.label)
-- },
-- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
-- (0, import_react4.createElement)(
-- "span",
-- { className: "bt-q-body" },
-- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label),
-- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null
-- ),
-- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null
-+function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
-+ if (whole && bots.length === 1) {
-+ return (0, import_react8.createElement)(
-+ "div",
-+ { className: "bt-event" },
-+ (0, import_react8.createElement)(
-+ "button",
-+ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
-+ (0, import_react8.createElement)("span", null, prefix),
-+ (0, import_react8.createElement)(BotMark, { bot: bots[0], size: 16 }),
-+ (0, import_react8.createElement)("span", { className: "bt-event-name" }, bots[0].name)
-+ )
+ function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
+ if (whole && bots.length === 1) {
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-event" },
+- (0, import_react6.createElement)(
++ (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)
++ (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)
+ )
);
-- });
-- if (raised && field) {
-- rows.push((0, import_react4.createElement)(
-- "label",
-- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 },
-- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
-- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field)
-- ));
}
-- let foot = null;
-- if (raised && multi) {
-- foot = (0, import_react4.createElement)(
-+ const parts = [];
-+ parts.push((0, import_react8.createElement)("span", { key: "p" }, prefix));
-+ bots.forEach((bot, index) => {
-+ if (index > 0) parts.push((0, import_react8.createElement)("span", { key: `j${index}` }, joiner));
-+ parts.push((0, import_react8.createElement)(
-+ "button",
-+ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
-+ (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
-+ bot.name
-+ ));
-+ });
-+ return (0, import_react8.createElement)("div", { className: "bt-event" }, parts);
+ const parts = [];
+- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
++ parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix));
+ bots.forEach((bot, index) => {
+- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner));
+- parts.push((0, import_react6.createElement)(
++ if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner));
++ parts.push((0, import_react9.createElement)(
+ "button",
+ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
+- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
++ (0, import_react9.createElement)(BotMark, { bot, size: 16 }),
+ bot.name
+ ));
+ });
+- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts);
++ return (0, import_react9.createElement)("div", { className: "bt-event" }, parts);
+}
+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();
@@ -6403,7 +4351,9 @@ diff --git a/client.js b/client.js
+ const held = heldForm(item.text);
+ return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held));
+ });
-+}
+ }
+-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) {
+- const lead = (0, import_react6.createElement)(
+var HELD_SENDING_MS = 6e4;
+function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) {
+ if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0;
@@ -6418,73 +4368,35 @@ diff --git a/client.js b/client.js
+function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, seq }) {
+ const held = heldLineState(room2, { seq, botId: bot.id, text });
+ if (held === "rejected") {
-+ return (0, import_react8.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"))
++ return (0, import_react9.createElement)(
++ "div",
+ { className: "bt-event bt-not-sent", "data-flybot": "held-not-sent", role: "status" },
-+ (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
-+ (0, import_react8.createElement)("span", null, t("{name} · Not sent", { name: bot.name }))
- );
-- } else if ((raised || multi) && canSubmit) {
-- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
- }
-- return (0, import_react4.createElement)(
-- "form",
-- {
-- className: raised ? "bt-q bt-q-raised" : "bt-q",
-- "aria-labelledby": titleId,
-- onSubmit: (event) => {
-- event.preventDefault();
-- submit();
-- }
-- },
-- (0, import_react4.createElement)(
-- "div",
-- { className: "bt-q-head" },
-- (0, import_react4.createElement)(
-- "div",
-- { className: "bt-q-text" },
-- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
-- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
-- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
-- ),
-- (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
++ (0, import_react9.createElement)(BotMark, { bot, size: 16 }),
++ (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name }))
++ );
++ }
+ if (held === "held" || isHeldLine(room2, text, bot)) return null;
-+ const lead = (0, import_react8.createElement)(
-+ "button",
-+ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) },
-+ (0, import_react8.createElement)(BotMark, { bot, size: 22 })
-+ );
-+ const name = (0, import_react8.createElement)(
++ 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 })
++ (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)) } },
+ faceBotLabel(room2, roster, bot) ?? bot.name,
-+ room2?.admin === bot.id ? (0, import_react8.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
++ room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
+ );
-+ return (0, import_react8.createElement)(
-+ "div",
-+ { className: "bt-group-msg" },
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-group-msg" },
+- (0, import_react6.createElement)(
+ name,
-+ (0, import_react8.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
- );
- }
--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)(
++ (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
++ );
++}
+function remoteSpeaker(source, roster, sessionId) {
+ const handle = source.remote.handle;
+ const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle);
@@ -6524,86 +4436,56 @@ diff --git a/client.js b/client.js
+ const files = Array.isArray(source.attachments) ? source.attachments : [];
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ if (person) {
-+ return (0, import_react8.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_react9.createElement)(
++ "div",
+ { className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle },
+ // One short header: the person, and the Bot that carried the line only when it is known (#275).
-+ (0, import_react8.createElement)(
++ (0, import_react9.createElement)(
+ "span",
+ { className: "bt-group-name", title: handle },
+ person,
-+ carrier ? (0, import_react8.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null
++ carrier ? (0, import_react9.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null
+ ),
-+ (0, import_react8.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react8.createElement)("span", { className: "bt-lead", title: person }, (0, import_react8.createElement)(PersonMark, { name: person })) }),
-+ files.length ? (0, import_react8.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
- );
- }
-- return (0, import_react4.createElement)(
-+ const mark = (0, import_react8.createElement)(BotMark, { bot, size: 22 });
-+ const lead = chatId ? (0, import_react8.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react8.createElement)("span", { className: "bt-lead", title: handle }, mark);
-+ return (0, import_react8.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))
-- );
-- }))
++ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }),
++ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
++ );
++ }
++ const mark = (0, import_react9.createElement)(BotMark, { bot, size: 22 });
++ const lead = chatId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react9.createElement)("span", { className: "bt-lead", title: handle }, mark);
++ return (0, import_react9.createElement)(
++ "div",
+ { className: "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_react8.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react8.createElement)(
-+ "span",
-+ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
-+ name,
-+ (0, import_react8.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react8.createElement)(GlobeMark), handle)
-+ ),
-+ (0, import_react8.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }),
-+ files.length ? (0, import_react8.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
- );
- }
--function QuestionCard({ matched }) {
-- const pending = matched;
-- const questions = pending.questions ?? [];
-- const [index, setIndex] = (0, import_react4.useState)(0);
-- const [answers, setAnswers] = (0, import_react4.useState)([]);
-- const question = questions[index];
-- if (question === void 0) return null;
-- const onAnswer = (answer) => {
-- const next = [...answers, { id: question.id, ...answer }];
-- if (index + 1 < questions.length) {
-- setAnswers(next);
-- setIndex(index + 1);
-- return;
++ 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
++ 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 FirstContact({ source, roster, sessionId }) {
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ const who = person || remoteSpeaker(source, roster).name;
+ const bot = roster.byId[sessionId]?.name ?? t("your Bot");
-+ return (0, import_react8.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react8.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot })));
++ return (0, import_react9.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react9.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot })));
+}
+function GlobeMark() {
-+ return (0, import_react8.createElement)(
++ return (0, import_react9.createElement)(
+ "svg",
+ { width: 10, height: 10, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react8.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
-+ (0, import_react8.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
++ (0, import_react9.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
++ (0, import_react9.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ );
+}
+function RemoteFiles({ files, plugin, botId }) {
-+ const [busy, setBusy] = (0, import_react8.useState)("");
-+ const [error, setError] = (0, import_react8.useState)("");
++ const [busy, setBusy] = (0, import_react9.useState)("");
++ const [error, setError] = (0, import_react9.useState)("");
+ const download2 = async (file) => {
+ setBusy(file.id);
+ setError("");
@@ -6619,434 +4501,242 @@ diff --git a/client.js b/client.js
+ setError(String(reason?.message ?? reason));
+ } finally {
+ setBusy("");
- }
-- pending.answer({ answers: next });
- };
-- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
-+ return (0, import_react8.createElement)(
++ }
++ };
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-file-chips" },
-+ files.map((file) => (0, import_react8.createElement)(
++ files.map((file) => (0, import_react9.createElement)(
+ "button",
+ { key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) },
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
-+ error ? (0, import_react8.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
-+ );
++ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
+ );
}
--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 GroupEvent({ prefix, room: room2, name, roster, actions }) {
-+ return (0, import_react8.createElement)(
+ function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
"div",
-- { className: inline ? "bt-q-inline" : "bt-q-dock" },
-- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
-+ { className: "bt-event" },
-+ (0, import_react8.createElement)("span", null, prefix),
-+ room2 ? (0, import_react8.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react8.createElement)("span", null, name) : null
+ { className: "bt-event" },
+- (0, import_react6.createElement)("span", null, prefix),
+- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null
++ (0, import_react9.createElement)("span", null, prefix),
++ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null
);
}
--function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
-- const question = useRoster((value) => value.questions?.[sessionId]);
-- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true;
-- const loading = useSession?.(historyLoading) === true;
-- if (question === void 0 || question.callId && (drawn || loading)) return null;
-- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
-+function 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_react8.createElement)(
-+ "div",
-+ { className: "bt-event" },
-+ (0, import_react8.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react8.createElement)(MemoryIcon), label)
-+ );
+ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
+@@ -4005,14 +6064,14 @@
+ const target = other && other.id !== self?.id ? other : self;
+ if (!target) return null;
+ const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name });
+- return (0, import_react6.createElement)(
++ 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)
++ (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label)
+ );
}
--function AnsweredCell({ node }) {
-- const data = node.data ?? {};
-- const questions = Array.isArray(data.questions) ? data.questions : [];
-- if (questions.length === 0) {
-- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
-- }
-- const answers = Array.isArray(data.answers) ? data.answers : [];
-- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
-- key: question.id,
-- question,
-- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
-- })));
-+function PartLine({ time }) {
-+ return (0, import_react8.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react8.createElement)("span", null, dayLabel(time)) : null);
+ 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);
}
--
--// src/client/secret-card.js
--var import_react5 = require("react");
--var MIN_VALUE = 4;
--var NONE = {};
--var KeyIcon = () => (0, import_react5.createElement)(
-- "svg",
-- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
-- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
--);
--var LockIcon = () => (0, import_react5.createElement)(
-- "svg",
-- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
-- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
--);
--var WarnIcon = () => (0, import_react5.createElement)(
-- "svg",
-- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
-- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
-- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
--);
--var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
--var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
--var scopeLabel = (scope, roster) => {
-- if (scope === "all") return t("All Bots");
-- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
-- return names.length === 0 ? t("No Bot") : names.join(t(" and "));
-+var scrollParent = (node) => {
-+ for (let at = node.parentElement; at; at = at.parentElement) {
-+ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at;
-+ }
-+ return null;
+ var scrollParent = (node) => {
+ for (let at = node.parentElement; at; at = at.parentElement) {
+@@ -4021,11 +6080,11 @@
+ return null;
};
--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 EarlierParts({ sessionId, roster, actions }) {
-+ const [view, setView] = (0, import_react8.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
-+ const top = (0, import_react8.useRef)(null);
-+ const busy = (0, import_react8.useRef)(false);
-+ const alive = (0, import_react8.useRef)(true);
-+ const restore = (0, import_react8.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_react8.useEffect)(() => {
-+ alive.current = true;
-+ load(void 0);
-+ return () => {
-+ alive.current = false;
-+ };
-+ }, [sessionId]);
-+ (0, import_react8.useLayoutEffect)(() => {
-+ const pending = restore.current;
-+ restore.current = null;
-+ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd;
-+ }, [view.items]);
-+ (0, import_react8.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_react8.createElement)(
-+ "div",
-+ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading },
-+ (0, import_react8.createElement)(
-+ "div",
-+ { ref: top, className: "bt-earlier-top" },
-+ view.loading ? (0, import_react8.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react8.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react8.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_react8.createElement)(PartLine, { key, time: row.time });
-+ case "user":
-+ return (0, import_react8.createElement)("div", { key, className: "bt-urow" }, (0, import_react8.createElement)(
-+ "div",
-+ { className: "bt-ububble" },
-+ row.text ? (0, import_react8.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null,
-+ row.images ? (0, import_react8.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null
-+ ));
-+ case "answer":
-+ return (0, import_react8.createElement)(AnsweredCell, { key, node: { data: row } });
-+ case "bot":
-+ return (0, import_react8.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions });
-+ case "event": {
-+ if (row.role === "report") return (0, import_react8.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_react8.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
-+ if (selfId && other.id) actions.openExchange(selfId, other.id);
-+ }, whole: true });
-+ }
-+ default:
-+ return null;
-+ }
-+ }),
-+ (0, import_react8.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_react8.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_react8.createElement)(import_react8.Fragment, null, (0, import_react8.createElement)("div", { className: "bt-open-new" }, (0, import_react8.createElement)(NewDivider), (0, import_react8.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
-+ function triggerBody() {
+ function EarlierParts({ sessionId, roster, actions }) {
+- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
+- const top = (0, import_react6.useRef)(null);
+- const busy = (0, import_react6.useRef)(false);
+- const alive = (0, import_react6.useRef)(true);
+- const restore = (0, import_react6.useRef)(null);
++ const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
++ const top = (0, import_react9.useRef)(null);
++ const busy = (0, import_react9.useRef)(false);
++ const alive = (0, import_react9.useRef)(true);
++ const restore = (0, import_react9.useRef)(null);
+ const load = (cursor) => {
+ if (busy.current) return;
+ busy.current = true;
+@@ -4041,19 +6100,19 @@
+ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
+ });
+ };
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ alive.current = true;
+ load(void 0);
+ return () => {
+ alive.current = false;
+ };
+ }, [sessionId]);
+- (0, import_react6.useLayoutEffect)(() => {
++ (0, import_react9.useLayoutEffect)(() => {
+ const pending = restore.current;
+ restore.current = null;
+ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd;
+ }, [view.items]);
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ const node = top.current;
+ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0;
+ const observer = new IntersectionObserver((entries2) => {
+@@ -4069,35 +6128,35 @@
+ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text);
+ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item);
+ }
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading },
+- (0, import_react6.createElement)(
++ (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
++ 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 });
++ 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)(
++ 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
++ 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 } });
++ 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 });
++ 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 });
++ 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: () => {
++ return (0, import_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
+ if (selfId && other.id) actions.openExchange(selfId, other.id);
+ }, whole: true });
+ }
+@@ -4105,7 +6164,7 @@
+ return null;
+ }
+ }),
+- (0, import_react6.createElement)(PartLine, { time: view.startedAt })
++ (0, import_react9.createElement)(PartLine, { time: view.startedAt })
+ );
+ }
+ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
+@@ -4116,7 +6175,7 @@
+ const deferred = source.kind === "bot" && source.deferred === true;
+ const relayed = isRelayContext(data);
+ const turn = node.location?.turn?.turn ?? data.turn;
+- (0, import_react6.useLayoutEffect)(() => {
++ (0, import_react9.useLayoutEffect)(() => {
+ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
+ }, [relayed, sessionId, turn, source.senderSessionId]);
+ const order = useSessions((list) => {
+@@ -4135,23 +6194,31 @@
+ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : "";
+ });
+ const body = triggerBody();
+- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
++ 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() {
+ if (source.form === "remote-chat") return null;
+ if (source.shown === true) return null;
+ if (source.remote?.handle) {
-+ const message = (0, import_react8.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_react8.createElement)(import_react8.Fragment, null, (0, import_react8.createElement)(FirstContact, { source, roster, sessionId }), message) : message;
++ 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_react8.createElement)(EarlierParts, { sessionId, roster, actions });
+ if (source.kind === "bot") {
+- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions });
+- if (source.role === "kickoff" || source.role === "room") return null;
++ if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions });
+ if (source.role === "room" || source.role === "kickoff") return null;
-+ if (source.role === "mention") return (0, import_react8.createElement)("div", { className: "bt-event" }, (0, import_react8.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 === "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.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_react8.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_react8.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_react8.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
-+ if (source.role === "retry" || source.retried === true) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text"));
-+ const run = relayed ? commRunOf(
-+ order.split(",").filter(Boolean).map(Number),
-+ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])),
-+ turn,
-+ hiddenTurns
-+ ) : null;
-+ if (run === "folded") return null;
-+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
-+ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1);
-+ const bots = [sender];
-+ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
-+ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
-+ }
-+ return (0, import_react8.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
-+ });
+ const room2 = roster.roomsById[sessionId];
+ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
++ return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
+ }
+ if (source.role === "report") {
+- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
++ return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
+ }
+- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
+- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text"));
++ if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
++ if (source.role === "retry" || source.retried === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text"));
+ const run = relayed ? commRunOf(
+ order.split(",").filter(Boolean).map(Number),
+ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])),
+@@ -4165,7 +6232,7 @@
+ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
+ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
+ }
+- return (0, import_react6.createElement)(EventLine, {
++ 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 +6241,24 @@
}
-- });
-+ if (source.kind === "schedule") {
-+ const prompts = reminderPrompts(contentText(data.content));
-+ return (0, import_react8.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_react8.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
-+ }
+ if (source.kind === "schedule") {
+ const prompts = reminderPrompts(contentText(data.content));
+- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
++ return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
+ }
+ if (source.kind === "user-question-reply") return null;
+- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
++ return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
+ }
}
--function 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 NewDivider() {
-+ return (0, import_react8.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react8.createElement)("span", null, t("New")));
+ function NewDivider() {
+- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New")));
++ return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New")));
}
--function 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 TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
-+ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
-+ (0, import_react8.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_react8.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_react8.createElement)(NewDivider) : null;
+ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
+ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ if (!open) turnProcess.setOpen(true);
+ }, [open, turnProcess]);
+ const turn = node?.location?.turn?.turn ?? node?.data?.turn;
+ const start = node?.location?.turn?.start?.time;
+ const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn));
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ actions.noteTurnTime(sessionId, turn, "first", start);
+ }, [sessionId, turn, start]);
+ const visit = useUi((state) => state.visits?.[sessionId]);
+@@ -4199,11 +6266,63 @@
+ const own = state[sessionId]?.[turn];
+ return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered;
+ });
+- return fresh ? (0, import_react6.createElement)(NewDivider) : null;
++ return fresh ? (0, import_react9.createElement)(NewDivider) : null;
}
--function 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 Nothing() {
-+ return null;
+ function Nothing() {
+ return null;
}
--function SettledSecret({ bot, request, roster }) {
-- const canceled = request.status === "canceled";
-- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name });
-- return (0, import_react5.createElement)(
-- "div",
-- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status },
-- (0, import_react5.createElement)(
-- "div",
-- { className: "bt-q-head" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })),
-- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)(
-- "p",
-- { className: "bt-q-title" },
-- text,
-- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
-- )),
-- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon))
-- )
-- );
+var 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([
@@ -7084,132 +4774,28 @@ diff --git a/client.js b/client.js
+function isHiddenToolRemoval(node) {
+ const content = node?.data?.content;
+ if (!Array.isArray(content) || content.length === 0) return false;
-+ return content.every(isHiddenNotice);
-+}
-+function ContextCell({ node, useRoster }) {
-+ useRoster?.((roster) => (roster.pluginTools ?? []).join("\n"));
-+ if (isHiddenToolRemoval(node)) return null;
-+ const content = Array.isArray(node?.data?.content) ? node.data.content : [];
-+ const parts = [];
-+ for (const block of content) {
-+ 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}`);
-+ }
-+ if (parts.length === 0) return null;
-+ return (0, import_react8.createElement)("div", { className: "bt-event" }, parts.join(" · "));
- }
--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 });
-+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 SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) {
-- const bot = roster.byId[sessionId];
-- if (!bot) return null;
-- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId);
-- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions }));
-- if (done && /already saved/.test(contentText(content))) {
-- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
-+function nextTurnStart(turns, turn) {
-+ let next;
-+ for (const key in turns) {
-+ const candidate = Number(key);
-+ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate;
- }
-- return null;
--}
--function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
-- const roster = useRoster((value) => value);
-- const shown = useCards?.((state) => state[sessionId]) ?? NONE;
-- const loading = useSession?.(historyLoading) === true;
-- const bot = roster.byId[sessionId];
-- if (!bot || currentPart(bot) !== sessionId) return null;
-- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0);
-- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry));
-- if (waiting.length === 0) return null;
-- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
-+ return next === void 0 ? void 0 : turns[next].first;
- }
--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 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;
++ return content.every(isHiddenNotice);
++}
++function ContextCell({ node, useRoster }) {
++ useRoster?.((roster) => (roster.pluginTools ?? []).join("\n"));
++ if (isHiddenToolRemoval(node)) return null;
++ const content = Array.isArray(node?.data?.content) ? node.data.content : [];
++ const parts = [];
++ for (const block of content) {
++ 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}`);
++ }
++ 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 +6352,19 @@
+ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null;
}
--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
-- );
-+var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
+ 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 ?? {}) {
@@ -7217,701 +4803,344 @@ diff --git a/client.js b/client.js
+ if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true;
+ }
+ return false;
- }
--function SecretRow({ secret, roster, actions, run, open, onToggle }) {
-- const [confirm, setConfirm] = (0, import_react5.useState)(false);
-- (0, import_react5.useEffect)(() => {
-- if (!open) setConfirm(false);
-- }, [open]);
-- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you");
-- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean);
-- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · ");
-- return (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-item", "data-open": open ? "" : void 0 },
-- (0, import_react5.createElement)(
-- "button",
-- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
-- (0, import_react5.createElement)(
-- "span",
-- { className: "bt-set-copy" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
-- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta)
-- ),
-- (0, import_react5.createElement)(
-- "span",
-- { className: "bt-set-control" },
-- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
-- )
-- ),
-- open ? (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-edit" },
-- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
-- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
-- (0, import_react5.createElement)(
-- "div",
-- { className: "bt-secret-row-actions" },
-- confirm ? [
-- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
-- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
-- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete"))
-- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete"))
-- )
-- ) : null
++}
+var 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_react8.useEffect)(() => {
-+ actions.noteTurnTime(sessionId, turn, "end", time);
-+ }, [sessionId, turn, time]);
-+ const label = useTurnClock((state) => {
-+ const start = nextTurnStart(state[sessionId] ?? {}, turn);
-+ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : "";
-+ });
-+ const visit = useUi((state) => state.visits?.[sessionId]);
-+ const fresh = useTurnClock((state) => {
-+ if (typeof visit?.seen !== "number" || time > visit.seen) return false;
-+ const start = nextTurnStart(state[sessionId] ?? {}, turn);
-+ return start !== void 0 && start > visit.seen && start <= visit.entered;
-+ });
-+ if (!label && !fresh) return null;
-+ return (0, import_react8.createElement)(
-+ import_react8.Fragment,
-+ null,
-+ label ? (0, import_react8.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
-+ fresh ? (0, import_react8.createElement)(NewDivider) : null
+ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
+ const turn = location2.turn;
+ const time = location2.end.time;
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ actions.noteTurnTime(sessionId, turn, "end", time);
+ }, [sessionId, turn, time]);
+ const label = useTurnClock((state) => {
+@@ -4250,15 +6378,15 @@
+ return start !== void 0 && start > visit.seen && start <= visit.entered;
+ });
+ if (!label && !fresh) return null;
+- return (0, import_react6.createElement)(
+- import_react6.Fragment,
++ return (0, import_react9.createElement)(
++ import_react9.Fragment,
+ null,
+- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
+- fresh ? (0, import_react6.createElement)(NewDivider) : null
++ label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
++ fresh ? (0, import_react9.createElement)(NewDivider) : null
);
}
--function 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"))
-- )
-- );
-+function Shimmer({ children }) {
-+ return (0, import_react8.createElement)("span", { className: "bt-shimmer" }, children);
+ function Shimmer({ children }) {
+- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
++ return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children);
}
--function SecretsPage({ roster, actions, run, Section }) {
-- const [open, setOpen] = (0, import_react5.useState)(null);
-- const [adding, setAdding] = (0, import_react5.useState)(false);
-- const secrets = roster.secrets ?? [];
-- return [
-- (0, import_react5.createElement)(
-- "p",
-- { key: "about", className: "bt-secret-about" },
-- t("Keys your Bots asked for. Bots never see the values: they use them as "),
-- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"),
-- t(" in their shell, and every value is replaced by "),
-- (0, import_react5.createElement)("code", null, "[secret:NAME]"),
-- t(" before anything goes to a model.")
-- ),
-- (0, import_react5.createElement)(
-- Section,
-- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
-- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
-- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
-- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
-- ),
-- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key")))
-- ];
-+var 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) };
-+ }
+ var PHRASES = {
+ thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"],
+@@ -4320,19 +6448,22 @@
}
--var SECRET_CSS = `
--.bt-secret-head{align-items:center;gap:10px}
--.bt-secret-who{position:relative;flex:none;display:inline-flex}
--.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)}
--.bt-secret-lock svg{width:11px;height:11px}
--.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)}
--.bt-secret-body{display:flex;flex-direction:column;gap:8px}
--.bt-secret-field{align-items:center;min-height:36px}
--.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em}
--.bt-secret-input[data-masked]{-webkit-text-security:disc}
--.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0}
--.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)}
--.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}}
--.bt-secret-glide[data-at="1"]{transform:translateX(100%)}
--.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease}
--.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500}
--.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
--/* A key card asks the user to act, so the process group carrying it stays open in every
-- work-details mode (same override as .bt-group-replies in styles.js). */
--[data-step-process]:has(.bt-secret)>div:first-child{display:none!important}
--[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
--.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)}
--.bt-secret-ok svg{width:13px;height:13px;color:#16a34a}
--@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}}
--.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
--.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)}
--.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}
--.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)}
--.bt-secret-error{font-size:12px;line-height:16px;color:#e02135}
--.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default}
--.bt-secret-done .bt-q-head{align-items:center}
--.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500}
--.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)}
--.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
--.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards}
--@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}}
--@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}}
--.bt-secret-event{display:inline-flex;align-items:center;gap:5px}
--.bt-secret-dock{flex-direction:column;align-items:center}
--.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))}
--.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)}
--.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)}
--.bt-secret-title svg{flex:none;color:var(--bt-ink-2)}
--.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)}
--.bt-secret-faces{display:inline-flex}
--.bt-secret-faces>*+*{margin-left:-6px}
--.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
--.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px}
--.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)}
--.bt-secret-radios{display:flex;gap:16px;font-size:13px}
--.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer}
--.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px}
--.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease}
--.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)}
--.bt-secret-bot svg{width:13px;height:13px}
--.bt-secret-inline{display:flex;gap:8px;align-items:center}
--.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
--.bt-secret-row-actions .bt-set-hint{flex:1}
--.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px}
--.bt-secret-danger svg{width:13px;height:13px}
--.bt-secret-add .bt-input{height:34px}
--@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
--`;
--
--// src/client/turn-reasoning.js
--function turnReasoning(steps, current) {
-- const members = steps.length ? steps : [current];
-- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
-- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
-+function workPhrase(tool) {
-+ if (!tool) return t("Thinking about it");
+ function workPhrase(tool) {
+ if (!tool) return t("Thinking about it");
+- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool });
- }
--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)(
++}
+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_react8.useEffect)(() => {
-+ if (!key) return void 0;
-+ actions.noteLive(sessionId, id, JSON.parse(key));
-+ return () => actions.noteLive(sessionId, id, null);
-+ }, [sessionId, id, key]);
-+}
+ }
+ function useLiveNote(actions, sessionId, id, entry) {
+ const key = entry ? JSON.stringify(entry) : "";
+- (0, import_react6.useEffect)(() => {
++ (0, import_react9.useEffect)(() => {
+ if (!key) return void 0;
+ actions.noteLive(sessionId, id, JSON.parse(key));
+ return () => actions.noteLive(sessionId, id, null);
+ }, [sessionId, id, key]);
+ }
+-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) {
+- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null });
+- (0, import_react6.useEffect)(() => {
+function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) {
-+ const [live, setLive] = (0, import_react8.useState)({ replies: [] });
-+ (0, import_react8.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 [live, setLive] = (0, import_react9.useState)({ replies: [] });
++ (0, import_react9.useEffect)(() => {
+ if (done) return void 0;
+ let stop = false;
+ let timer;
+@@ -4341,10 +6472,10 @@
+ const result = await actions.roomProgress(sessionId);
+ if (stop) return;
+ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
+- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : "";
+ const key = entry ? JSON.stringify(entry.replies) : "";
-+ if (entry && key !== seen) {
-+ seen = key;
+ if (entry && key !== seen) {
+ seen = key;
+- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null });
+ setLive({ replies: entry.replies ?? [] });
-+ }
-+ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
-+ };
-+ void tick();
-+ return () => {
-+ stop = true;
-+ clearTimeout(timer);
-+ };
-+ }, [done, sessionId, callId]);
-+ let replies = live.replies;
-+ if (done) {
-+ try {
-+ replies = JSON.parse(contentText(content)).replies ?? [];
-+ } catch {
-+ replies = [];
-+ }
-+ }
+ }
+ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
+ };
+@@ -4362,71 +6493,89 @@
+ replies = [];
+ }
+ }
+- const pending = done || !live.speaking ? [] : [live.speaking];
+- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0;
+- if (replies.length === 0 && pending.length === 0) return null;
+ if (replies.length === 0) return null;
-+ const room2 = roster.roomsById[sessionId];
-+ return (0, import_react8.createElement)(
+ const room2 = roster.roomsById[sessionId];
+- return (0, import_react6.createElement)(
++ return (0, import_react9.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)(
-+ { className: "bt-group-replies" },
-+ replies.map((reply, index) => {
+ { className: "bt-group-replies" },
+ replies.map((reply, index) => {
+- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" };
+ const stored = roster.byId[reply.botId];
+ const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored;
+ const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" };
-+ if (reply.kind === "user") {
+ if (reply.kind === "user") {
+- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)(
+ const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId;
+ if (reply.other) {
-+ return (0, import_react8.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)(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_react8.createElement)(
-+ "div",
++ 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" })
+- ));
+ { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who },
-+ who ? (0, import_react8.createElement)("span", { className: "bt-uauthor" }, who) : null,
-+ (0, import_react8.createElement)(
++ who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null,
++ (0, import_react9.createElement)(
+ "div",
+ { className: "bt-ububble" },
-+ (0, import_react8.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" })
++ (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" })
+ ),
-+ room2?.origin === "face-to-face" ? (0, import_react8.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react8.createElement)(PersonMark, { name: who || void 0 })) : null
++ room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null
+ );
-+ }
-+ if (reply.kind === "failed") {
-+ return (0, import_react8.createElement)(
-+ "div",
+ }
+ if (reply.kind === "failed") {
+- return (0, import_react6.createElement)(
++ 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(" · ") },
+ { 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_react8.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_react8.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_react8.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_react8.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_react8.createElement)(
+ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) })
+ );
+ }
+ if (reply.kind === "lookup") {
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
+ "div",
+ { key: `${reply.botId}-${index}`, className: "bt-event" },
+ reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name })
+ );
+ }
+ if (reply.kind === "missed") {
+- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
++ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
+ }
+ if (reply.kind === "resumed") {
+- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name }));
++ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name }));
+ }
+ if (reply.kind === "cut") {
+- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name }));
++ return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name }));
+ }
+ if (reply.kind === "approval" || reply.kind === "working") {
+- return (0, import_react6.createElement)(
++ return (0, import_react9.createElement)(
"div",
-- { className: "bt-msg-line" },
-- lead && last ? lead : null,
+ { key: `${reply.botId}-${index}`, className: "bt-event" },
- (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
++ (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 }))
++ (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 }));
++ 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 }))
- );
-+ { key: `${reply.botId}-${index}`, className: "bt-event" },
-+ (0, import_react8.createElement)(
-+ "button",
-+ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) },
-+ (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
-+ (0, import_react8.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_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { 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") {
-+ return (0, import_react8.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text));
++ return (0, import_react9.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text));
+ }
+ const remote = reply.remote || reply.handle && !roster.byId[reply.botId];
+ if (remote) {
-+ return (0, import_react8.createElement)(RemoteMessage, {
++ return (0, import_react9.createElement)(RemoteMessage, {
+ key: `${reply.botId || reply.handle}-${index}`,
-+ source: { remote: { handle: reply.handle || reply.botId }, senderName: reply.name || reply.who, senderSessionId: reply.botId },
-+ text: remoteMessageText(reply.text),
-+ roster,
-+ actions,
-+ sessionId,
-+ msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0,
-+ useUi
-+ });
-+ }
-+ return (0, import_react8.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 });
- })
- );
- }
--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);
-- };
-- }, [open]);
--}
--function MessageTools({ text, name, sessionId, react, actions }) {
-- const [panel, setPanel] = (0, import_react6.useState)(null);
-- const [place, setPlace] = (0, import_react6.useState)(null);
-- const [copied, setCopied] = (0, import_react6.useState)(false);
-- const tools = (0, import_react6.useRef)(null);
-- const pop = (0, import_react6.useRef)(null);
-- const close = () => setPanel(null);
-- useDismiss(panel !== null, close, [tools, pop]);
-- const open = (kind, event) => {
-- if (panel === kind) {
-- close();
-- return;
-- }
-- 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);
-+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();
++ source: { remote: { handle: reply.handle || reply.botId }, senderName: reply.name || reply.who, senderSessionId: reply.botId },
++ text: remoteMessageText(reply.text),
++ roster,
++ actions,
++ sessionId,
++ msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0,
++ useUi
++ });
++ }
++ 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 +6584,52 @@
+ function callTree(root) {
+ const calls = [];
+ const seen = /* @__PURE__ */ new Set();
+- const visit = (call2) => {
+- const id = call2.callId ?? call2;
+ const visit = (call4) => {
+ const id = call4.callId ?? call4;
-+ if (seen.has(id)) return;
-+ seen.add(id);
+ if (seen.has(id)) return;
+ seen.add(id);
+- calls.push(call2);
+- for (const child of call2.subCalls ?? []) visit(child);
+ calls.push(call4);
+ for (const child of call4.subCalls ?? []) visit(child);
};
-- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
-- close();
-- run();
-- } }, (0, import_react6.createElement)(Icon), label);
-- return (0, import_react6.createElement)(
-- "div",
-- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
-- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null,
-- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null,
-- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)),
-- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
-- "div",
-- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place },
-- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => {
-- close();
-- react(emoji);
-- } }, emoji))
-- ), document.body) : null,
-- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
-- "div",
-- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place },
-- item(t("Copy"), CopyIcon, copy2),
-- item(t("Download"), DownloadIcon, download2)
-- ), document.body) : null
-- );
-+ visit(root);
-+ return calls;
+ visit(root);
+ return calls;
}
--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 useShownCards(actions, sessionId, callIds) {
-+ const key = JSON.stringify(callIds);
-+ (0, import_react8.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 useShownCards(actions, sessionId, callIds) {
+ const key = JSON.stringify(callIds);
+- (0, import_react6.useLayoutEffect)(() => {
++ (0, import_react9.useLayoutEffect)(() => {
+ const ids = JSON.parse(key);
+ if (ids.length === 0 || !actions.noteCards) return void 0;
+ actions.noteCards(sessionId, ids, 1);
+ return () => actions.noteCards(sessionId, ids, -1);
+ }, [sessionId, key]);
}
--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 renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) {
+- const tool = toolName(call2);
+- const done = call2.kind === "tool-result";
+- const { text: arg, value: argValue } = toolArgs(call2);
+function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
+ const tool = toolName(call4);
+ const done = call4.kind === "tool-result";
+ const { text: arg, value: argValue } = toolArgs(call4);
-+ const self = roster.byId[sessionId];
-+ switch (tool) {
-+ case "message_bot": {
-+ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
-+ if (done && call4.isError) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
-+ return (0, import_react8.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 && call4.isError) return (0, import_react8.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
-+ return (0, import_react8.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_react8.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_react8.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
-+ }
-+ case "group_relay":
-+ return (0, import_react8.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
-+ case "create_group":
-+ case "update_group":
-+ case "delete_group":
-+ case "post_to_group": {
-+ const verbs = {
-+ create_group: ["Created group", "Couldn't create the group"],
-+ update_group: ["Updated group", "Couldn't update the group"],
-+ delete_group: ["Deleted group", "Couldn't delete the group"],
-+ post_to_group: ["Posted in", "Couldn't post in the group"]
-+ }[tool];
-+ if (!done) return null;
+ const self = roster.byId[sessionId];
+ switch (tool) {
+ case "message_bot": {
+ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
+- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
++ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
++ return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+ }
+ case "stop_bot": {
+ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
+- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
++ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
++ return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+ }
+ case "create_bot": {
+ const created = findBot(arg("name"));
+ if (!done || created === void 0) return null;
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
++ return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
+ }
+ case "update_bot": {
+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
+- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
++ return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
+ }
+ case "group_relay":
+- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork });
++ return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
+ case "create_group":
+ case "update_group":
+ case "delete_group":
+@@ -4492,64 +6641,80 @@
+ post_to_group: ["Posted in", "Couldn't post in the group"]
+ }[tool];
+ if (!done) return null;
+- const result = contentText(call2.content);
+ const result = contentText(call4.content);
-+ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
-+ if (call4.isError || !named) return (0, import_react8.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_react8.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 !== call4.callId) return (0, import_react8.createElement)("span", { className: "bt-q-gone", hidden: true });
-+ return (0, import_react8.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
-+ }
-+ case "request_secret":
-+ return (0, import_react8.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 {
+ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
+- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
++ if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
+ const name = named[2];
+ const room2 = roster.rooms.find((entry) => entry.name === name);
+- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
++ return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
+ }
+ case "ask_user": {
+ const question = roster.questions?.[sessionId];
+- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true });
+- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
++ if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
++ return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
+ }
+ case "request_secret":
+- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions });
++ return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions });
+ case "ask_user_question": {
+ if (!done) return null;
+ let parsed = null;
+ try {
+- parsed = JSON.parse(contentText(call2.content));
+ 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_react8.createElement)("div", { className: "bt-q-inline" }, (0, import_react8.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_react8.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null;
-+ }
-+ case "remember":
-+ case "forget":
-+ return done && !call4.isError ? (0, import_react8.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
-+ default:
-+ return null;
-+ }
+ } catch {
+ parsed = null;
+ }
+ const answer = parsed?.answers?.[0];
+ const question = argValue("questions")?.[0];
+ if (!answer) return null;
+- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer }));
++ 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;
++ 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;
++ return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
+ default:
+ return null;
+ }
}
--function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) {
+-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) {
+function ToolCell({ node, sessionId, useRoster, useUi, 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
-- );
-+ const root = node.data.root;
-+ const turn = node.location?.turn?.turn ?? root.turn;
-+ const hidden = isHiddenTurn(roster, sessionId, turn);
+ const root = node.data.root;
+ const turn = node.location?.turn?.turn ?? root.turn;
+ const hidden = isHiddenTurn(roster, sessionId, turn);
+- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase());
+ const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId);
-+ const calls = hidden ? [] : callTree(root);
+ const calls = hidden ? [] : callTree(root);
+- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2)));
+- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result");
+ const drawn = calls.filter((call4) => DRAWN.has(toolName(call4)));
+ const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result");
-+ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null;
-+ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note);
+ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null;
+ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note);
+- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId));
+ useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId));
-+ if (drawn.length === 0) return null;
+ if (drawn.length === 0) return null;
+- const env = { sessionId, roster, actions, useUi, useWork, findBot };
+- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env))));
+ const env = { sessionId, roster, actions, useUi, findBot };
-+ return (0, import_react8.createElement)(import_react8.Fragment, null, drawn.map((call4, index) => (0, import_react8.createElement)(import_react8.Fragment, { key: call4.callId ?? index }, renderCall(call4, env))));
- }
--function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
-- if (whole && bots.length === 1) {
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-event" },
-- (0, import_react6.createElement)(
-- "button",
-- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
-- (0, import_react6.createElement)("span", null, prefix),
-- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }),
-- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name)
-- )
-- );
-- }
-- const parts = [];
-- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
-- bots.forEach((bot, index) => {
-- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner));
-- parts.push((0, import_react6.createElement)(
-- "button",
-- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
-- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
-- bot.name
-- ));
-- });
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts);
++ 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))));
++}
+
+// src/client/awaiting.js
+var AWAIT_REPLY_MS = 10 * 60 * 1e3;
@@ -7919,24 +5148,7 @@ diff --git a/client.js b/client.js
+ const at = roster?.remoteAwaiting?.[sessionId];
+ if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false;
+ return now - at < AWAIT_REPLY_MS;
- }
--function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) {
-- const lead = (0, import_react6.createElement)(
-- "button",
-- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) },
-- (0, import_react6.createElement)(BotMark, { bot, size: 22 })
-- );
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-group-msg" },
-- (0, import_react6.createElement)(
-- "span",
-- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
-- bot.name,
-- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
-- ),
-- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
-- );
++}
+
+// src/client/remote-approval-wait.js
+var REMOTE_APPROVAL_LABEL = "Waiting for their approval";
@@ -7944,53 +5156,35 @@ 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_react9 = 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 GroupEvent({ prefix, room: room2, name, roster, actions }) {
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-event" },
-- (0, import_react6.createElement)("span", null, prefix),
-- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null
-- );
+
+ // src/client/activity-row.js
+-var import_react7 = require("react");
++var import_react10 = require("react");
+ var import_react_dom2 = require("react-dom");
+ var PHRASE_HOLD_MS = 800;
+ var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]);
+@@ -4559,9 +6724,71 @@
+ 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 RUNNING_HOST = "[data-chat-flow] > [data-chat-running]";
+var TAIL_HOST = "tail";
+var REMOTE_GRACE_MS = 1500;
+function useAwaitingReply(roster, sessionId) {
+ const at = roster?.remoteAwaiting?.[sessionId];
-+ const [, tick] = (0, import_react9.useState)(0);
++ const [, tick] = (0, import_react10.useState)(0);
+ const shown = awaitingReply(roster, sessionId);
-+ (0, import_react9.useEffect)(() => {
++ (0, import_react10.useEffect)(() => {
+ if (!shown) return void 0;
+ const timer = setTimeout(() => tick((n) => n + 1), Math.max(0, at + AWAIT_REPLY_MS - Date.now()) + 50);
+ return () => clearTimeout(timer);
+ }, [shown, at]);
+ return shown;
- }
--function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
-- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null;
-- const self = roster.byId[sessionId];
-- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim());
-- const other = team || !scope ? void 0 : findBot(scope);
-- const target = other && other.id !== self?.id ? other : self;
-- if (!target) return null;
-- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name });
-- return (0, import_react6.createElement)(
-- "div",
-- { className: "bt-event" },
-- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label)
-- );
++}
+function findHost(where) {
+ if (where !== TAIL_HOST) return document.querySelector(where);
+ const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]");
@@ -8006,10 +5200,10 @@ diff --git a/client.js b/client.js
+ return top;
+};
+function useReplyAfterSend(remote, sending) {
-+ const sendTurn = (0, import_react9.useRef)(-1);
-+ const wasSending = (0, import_react9.useRef)(false);
-+ const [replied, setReplied] = (0, import_react9.useState)(false);
-+ (0, import_react9.useEffect)(() => {
++ const sendTurn = (0, import_react10.useRef)(-1);
++ const wasSending = (0, import_react10.useRef)(false);
++ const [replied, setReplied] = (0, import_react10.useState)(false);
++ (0, import_react10.useEffect)(() => {
+ if (!remote) return void 0;
+ if (sending) {
+ wasSending.current = true;
@@ -8038,186 +5232,102 @@ diff --git a/client.js b/client.js
+ };
+ }, [remote, sending]);
+ return replied;
- }
--function PartLine({ time }) {
-- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null);
++}
+function useRunningHost(active, where = RUNNING_HOST) {
-+ const [host, setHost] = (0, import_react9.useState)(null);
-+ (0, import_react9.useEffect)(() => {
-+ if (!active) {
-+ setHost(null);
-+ return void 0;
-+ }
-+ let frame = 0;
-+ const find = () => {
-+ frame = 0;
++ const [host, setHost] = (0, import_react10.useState)(null);
++ (0, import_react10.useEffect)(() => {
+ if (!active) {
+ setHost(null);
+ return void 0;
+@@ -4569,7 +6796,7 @@
+ let frame = 0;
+ const find = () => {
+ frame = 0;
+- const node = document.querySelector("[data-chat-flow] > [data-chat-running]");
+ const node = findHost(where);
-+ setHost((prev) => prev === node ? prev : node);
-+ };
-+ const observer = new MutationObserver(() => {
-+ if (!frame) frame = requestAnimationFrame(find);
-+ });
-+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
-+ find();
-+ return () => {
-+ cancelAnimationFrame(frame);
-+ observer.disconnect();
-+ };
+ setHost((prev) => prev === node ? prev : node);
+ };
+ const observer = new MutationObserver(() => {
+@@ -4581,13 +6808,13 @@
+ cancelAnimationFrame(frame);
+ observer.disconnect();
+ };
+- }, [active]);
+ }, [active, where]);
-+ return host;
+ return host;
}
--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_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
-- const top = (0, import_react6.useRef)(null);
-- const busy = (0, import_react6.useRef)(false);
-- const alive = (0, import_react6.useRef)(true);
-- const restore = (0, import_react6.useRef)(null);
-- const load = (cursor) => {
-- if (busy.current) return;
-- busy.current = true;
-- setView((current) => ({ ...current, loading: true, failed: false }));
-- actions.earlier(sessionId, cursor).then((page) => {
-- busy.current = false;
-- if (!alive.current) return;
-- const scroller = top.current && scrollParent(top.current);
-- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop };
-- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt }));
-- }, () => {
-- busy.current = false;
-- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
-+function useRowEntrances(replacing) {
-+ const check = (0, import_react9.useRef)(replacing);
-+ check.current = replacing;
-+ (0, import_react9.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);
-+ }
-+ };
-+ const observer = new MutationObserver(() => {
-+ if (!frame) frame = requestAnimationFrame(scan);
- });
-- };
-- (0, import_react6.useEffect)(() => {
-- alive.current = true;
-- load(void 0);
-+ scan();
- return () => {
-- alive.current = false;
-+ cancelAnimationFrame(frame);
-+ observer.disconnect();
- };
-- }, [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)(
-+ }, []);
-+}
-+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_react9.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);
+ function useRowEntrances(replacing) {
+- const check = (0, import_react7.useRef)(replacing);
++ const check = (0, import_react10.useRef)(replacing);
+ check.current = replacing;
+- (0, import_react7.useEffect)(() => {
++ (0, import_react10.useEffect)(() => {
+ const seen = /* @__PURE__ */ new WeakSet();
+ const started = Date.now();
+ let column = null;
+@@ -4638,21 +6865,31 @@
+ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
+ const bot = roster.byId[sessionId];
+ const room2 = roster.roomsById[sessionId];
+- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
++ const 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 awaiting = useAwaitingReply(roster, sessionId);
-+ const sendRun = (0, import_react9.useRef)(false);
++ const sendRun = (0, import_react10.useRef)(false);
+ if (!running) sendRun.current = false;
+ else if (pendingInput > 0) sendRun.current = true;
+ const replied = useReplyAfterSend(bot?.remote !== void 0, running && sendRun.current);
+ const visible = bot?.remote !== void 0 ? !streaming && (running ? sendRun.current : awaiting && !replied) : running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
-+ const lastRun = (0, import_react9.useRef)({ running, send: false });
++ const lastRun = (0, import_react10.useRef)({ running, send: false });
+ const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running;
+ lastRun.current = { running, send: sendRun.current };
+ const steady = bridging;
-+ const tool = tools.at(-1);
-+ const inputRef = (0, import_react9.useRef)(0);
-+ const inputAt = Math.max(inputRef.current, pendingInput);
-+ inputRef.current = inputAt;
+ const tool = tools.at(-1);
+- const inputRef = (0, import_react7.useRef)(0);
++ const inputRef = (0, import_react10.useRef)(0);
+ const inputAt = Math.max(inputRef.current, pendingInput);
+ inputRef.current = inputAt;
+- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
+- const [shown, setShown] = (0, import_react7.useState)(null);
+ const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null;
+ const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
-+ const [shown, setShown] = (0, import_react9.useState)(null);
-+ const wantKey = want?.key ?? null;
-+ const markState = (0, import_react9.useRef)("thinking");
-+ if (want) markState.current = want.state;
-+ (0, import_react9.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_react9.useState)(null);
-+ const endedAt = (0, import_react9.useRef)(0);
-+ const shift = (0, import_react9.useRef)(null);
-+ const leaving = (0, import_react9.useRef)(false);
-+ leaving.current = phase !== null && !visible;
-+ (0, import_react9.useEffect)(() => {
-+ if (visible) {
-+ setPhase("in");
-+ return void 0;
-+ }
++ const [shown, setShown] = (0, import_react10.useState)(null);
+ const wantKey = want?.key ?? null;
+- const markState = (0, import_react7.useRef)("thinking");
++ const markState = (0, import_react10.useRef)("thinking");
+ if (want) markState.current = want.state;
+- (0, import_react7.useEffect)(() => {
++ (0, import_react10.useEffect)(() => {
+ if (wantKey === null || shown?.key === wantKey) return void 0;
+ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() });
+ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0;
+@@ -4663,24 +6900,29 @@
+ const timer = setTimeout(make, wait);
+ return () => clearTimeout(timer);
+ }, [wantKey, shown]);
+- const [phase, setPhase] = (0, import_react7.useState)(null);
+- const endedAt = (0, import_react7.useRef)(0);
+- const shift = (0, import_react7.useRef)(null);
+- const leaving = (0, import_react7.useRef)(false);
++ const [phase, setPhase] = (0, import_react10.useState)(null);
++ const 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)(() => {
++ (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);
+ let timer;
+ const leave = () => {
+ setPhase((current) => current === "in" ? "out" : current);
@@ -8226,113 +5336,78 @@ diff --git a/client.js b/client.js
+ setPhase((current) => current === "out" ? null : current);
+ }, 140);
+ };
-+ if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS);
-+ else leave();
-+ return () => clearTimeout(timer);
-+ }, [visible]);
-+ (0, import_react9.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_react9.useEffect)(() => {
-+ if (!running) actions.clearLive(sessionId);
-+ }, [running, sessionId]);
-+ useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
-+ const [now, setNow] = (0, import_react9.useState)(Date.now);
-+ (0, import_react9.useEffect)(() => {
-+ if (phase !== "in") return void 0;
-+ const timer = setInterval(() => setNow(Date.now()), 1e4);
-+ return () => clearInterval(timer);
-+ }, [phase]);
++ if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS);
++ else leave();
+ return () => clearTimeout(timer);
+ }, [visible]);
+- (0, import_react7.useLayoutEffect)(() => {
++ (0, import_react10.useLayoutEffect)(() => {
+ if (phase !== null) return;
+ endedAt.current = Date.now();
+ setShown(null);
+@@ -4691,31 +6933,37 @@
+ const delta = before - scroller.scrollTop;
+ if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" });
+ }, [phase]);
+- (0, import_react7.useEffect)(() => {
++ (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)(() => {
++ 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);
+ const remote = bot?.remote !== void 0;
+ const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST);
-+ const drawn = (0, import_react9.useRef)(false);
++ const drawn = (0, import_react10.useRef)(false);
+ const carry = drawn.current;
-+ (0, import_react9.useEffect)(() => {
++ (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_react9.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_react7.createElement)(
++ return (0, import_react_dom2.createPortal)((0, import_react10.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 })
-- );
+- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true },
+- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
+- (0, import_react7.createElement)(
+ { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true },
-+ bot ? (0, import_react9.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-+ (0, import_react9.createElement)(
-+ "span",
-+ { className: "bt-typing-text" },
-+ (0, import_react9.createElement)(
-+ "span",
-+ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
-+ (0, import_react9.createElement)(Shimmer, null, `${shown.text}…`),
-+ elapsed ? (0, import_react9.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
-+ )
-+ )
-+ ), host);
-+}
-+
-+// src/client/dsh-offer.js
-+var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]);
-+var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0];
-+var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
-+function showsDshRow(dsh, app) {
-+ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
-+ if (dsh.kind === "npm") return true;
-+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
-+}
-+
++ 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)(
++ (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
++ (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`),
++ elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
+ )
+ )
+ ), host);
+@@ -4731,49 +6979,1295 @@
+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
+ }
+
+// src/client/overlay-hooks.js
-+var import_react10 = require("react");
++var import_react11 = require("react");
+var escapeLayers = [];
+var onEscapeKey = (event) => {
+ if (event.key === "Escape") escapeLayers.at(-1)?.current();
+};
+function useEscape(onClose) {
-+ const ref = (0, import_react10.useRef)(onClose);
++ const ref = (0, import_react11.useRef)(onClose);
+ ref.current = onClose;
-+ (0, import_react10.useEffect)(() => {
++ (0, import_react11.useEffect)(() => {
+ if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey);
+ escapeLayers.push(ref);
+ return () => {
@@ -8340,35 +5415,10 @@ diff --git a/client.js b/client.js
+ if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey);
+ };
+ }, []);
- }
--function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
-- const roster = useRoster((value) => value);
-- const data = node.data;
-- const source = data.source !== null && typeof data.source === "object" ? data.source : {};
-- const selfId = roster.byId[sessionId]?.id ?? sessionId;
-- const deferred = source.kind === "bot" && source.deferred === true;
-- const relayed = isRelayContext(data);
-- const turn = node.location?.turn?.turn ?? data.turn;
-- (0, import_react6.useLayoutEffect)(() => {
-- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
-- }, [relayed, sessionId, turn, source.senderSessionId]);
-- const order = useSessions((list) => {
-- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline;
-- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : "";
-- });
-- const peers = useTurnClock((state) => {
-- if (!relayed) return "";
-- const turns = state[sessionId] ?? {};
-- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(",");
-- });
-- const visit = useUi((state) => state.visits?.[sessionId]);
-- const hiddenTurns = roster.exchangeTurns[sessionId];
-- const unreadOpening = useTurnClock((state) => {
-- const first = openingTurn(state[sessionId], order, hiddenTurns);
-- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : "";
++}
+function usePaneLeft() {
-+ const [paneLeft, setPaneLeft] = (0, import_react10.useState)(280);
-+ (0, import_react10.useLayoutEffect)(() => {
++ const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280);
++ (0, import_react11.useLayoutEffect)(() => {
+ const column = document.querySelector('[class*="_footArea"]')?.parentElement;
+ if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right));
+ }, []);
@@ -8376,7 +5426,7 @@ diff --git a/client.js b/client.js
+}
+
+// src/client/member-status.js
-+var import_react11 = require("react");
++var import_react12 = require("react");
+var NO_WORK = { bots: {}, rooms: {}, approvals: [] };
+var ROWS_MAX = 4;
+var stepIn = (work, botId, roomId, tool) => {
@@ -8395,74 +5445,12 @@ diff --git a/client.js b/client.js
+ const doing = workIn(room2, work, id);
+ const approvals = askedIn(room2, work, id, doing);
+ return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" };
- });
-- const body = triggerBody();
-- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
-- function triggerBody() {
-- if (source.kind === "bot") {
-- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions });
-- if (source.role === "kickoff" || source.role === "room") return null;
-- if (source.role === "secret" || deferred) return null;
-- if (source.role === "post") {
-- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null;
-- const room2 = roster.roomsById[sessionId];
-- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
-- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
-- }
-- if (source.role === "report") {
-- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
-- }
-- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
-- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text"));
-- const run = relayed ? commRunOf(
-- order.split(",").filter(Boolean).map(Number),
-- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])),
-- turn,
-- hiddenTurns
-- ) : null;
-- if (run === "folded") return null;
-- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
-- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1);
-- const bots = [sender];
-- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
-- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
-- }
-- return (0, import_react6.createElement)(EventLine, {
-- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"),
-- bots,
-- onOpen: (bot) => actions.openExchange(selfId, bot.id),
-- whole: true
-- });
-- }
-- if (source.kind === "schedule") {
-- const prompts = reminderPrompts(contentText(data.content));
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
-- }
-- if (source.kind === "user-question-reply") return null;
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
-- }
- }
--function NewDivider() {
-- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New")));
++ });
++}
+function busyMembers(room2, work, roster) {
+ const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]);
+ return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] }));
- }
--function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
-- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
-- (0, import_react6.useEffect)(() => {
-- if (!open) turnProcess.setOpen(true);
-- }, [open, turnProcess]);
-- const turn = node?.location?.turn?.turn ?? node?.data?.turn;
-- const start = node?.location?.turn?.start?.time;
-- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn));
-- (0, import_react6.useEffect)(() => {
-- actions.noteTurnTime(sessionId, turn, "first", start);
-- }, [sessionId, turn, start]);
-- const visit = useUi((state) => state.visits?.[sessionId]);
-- const fresh = useTurnClock((state) => {
-- const own = state[sessionId]?.[turn];
-- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered;
++}
+function splitBusy(busy) {
+ const here = busy.filter((entry) => entry.state === "work" && entry.here);
+ const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX);
@@ -8473,11 +5461,8 @@ diff --git a/client.js b/client.js
+ const entry = entries2.find((item) => item.botId === botId);
+ const room2 = roster.roomsById[roomId];
+ return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : [];
- });
-- return fresh ? (0, import_react6.createElement)(NewDivider) : null;
- }
--function Nothing() {
-- return null;
++ });
++}
+function summaryText(busy) {
+ const asks = busy.filter((entry) => entry.state === "ask");
+ const workers = busy.filter((entry) => entry.state === "work");
@@ -8486,56 +5471,41 @@ 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 };
- }
--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 Face({ entry, size }) {
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "span",
+ { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 },
-+ (0, import_react11.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
++ (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
+ );
- }
--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;
++}
+var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : "";
+function WorkRow({ bot, tool, step, label, at, now, onOpen }) {
+ const text = `${label} · ${workText(tool, step)}`;
+ const elapsed = elapsedSince(now, at);
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "button",
+ { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
-+ (0, import_react11.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }),
-+ (0, import_react11.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react11.createElement)(Shimmer, null, text)),
-+ elapsed ? (0, import_react11.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
++ (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 SummaryLine({ busy, now, onOpen }) {
+ const { waiting, doing } = summaryText(busy);
+ const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : "";
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "button",
+ { type: "button", className: "bt-mw-row bt-mw-line", "aria-label": [waiting, doing, elapsed].filter(Boolean).join(" · "), onClick: onOpen },
-+ (0, import_react11.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react11.createElement)(Face, { key: entry.id, entry, size: 16 }))),
-+ (0, import_react11.createElement)(
++ (0, import_react12.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react12.createElement)(Face, { key: entry.id, entry, size: 16 }))),
++ (0, import_react12.createElement)(
+ "span",
+ { className: "bt-mw-row-text" },
-+ waiting ? (0, import_react11.createElement)("span", { className: "bt-mw-ask" }, waiting) : null,
++ waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null,
+ waiting && doing ? " · " : null,
-+ doing ? (0, import_react11.createElement)(Shimmer, null, doing) : null
++ doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null
+ ),
-+ elapsed ? (0, import_react11.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
++ elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null
+ );
+}
+function MemberDock({ sessionId, useRoster, useWork, actions }) {
@@ -8548,21 +5518,20 @@ diff --git a/client.js b/client.js
+ const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : [];
+ const shown = busy.length > 0 || groups.length > 0;
+ const now = useNow(shown);
-+ const ref = (0, import_react11.useRef)(null);
++ const ref = (0, import_react12.useRef)(null);
+ useFlowColumn(ref, shown, sessionId);
+ if (!shown) return null;
+ if (room2) {
+ const { rows, rest } = splitBusy(busy);
+ const open = () => actions.showDetails(sessionId);
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "div",
+ { ref, className: "bt-mw-dock-stack" },
-+ rows.map((entry) => (0, import_react11.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })),
-+ rest.length > 0 ? (0, import_react11.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null
++ rows.map((entry) => (0, import_react12.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })),
++ rest.length > 0 ? (0, import_react12.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null
+ );
- }
-- return next === void 0 ? void 0 : turns[next].first;
-+ return (0, import_react11.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react11.createElement)(WorkRow, {
++ }
++ return (0, import_react12.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react12.createElement)(WorkRow, {
+ key: item.room.id,
+ bot,
+ tool: item.tool,
@@ -8572,17 +5541,9 @@ diff --git a/client.js b/client.js
+ now,
+ onOpen: () => actions.openSession(item.room.id)
+ })));
- }
--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 useFlowColumn(ref, active, key) {
-+ (0, import_react11.useLayoutEffect)(() => {
++ (0, import_react12.useLayoutEffect)(() => {
+ const node = ref.current;
+ const flow = document.querySelector("[data-chat-flow]");
+ if (!active || !node || !flow) return void 0;
@@ -8597,33 +5558,10 @@ diff --git a/client.js b/client.js
+ for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target);
+ return () => observer.disconnect();
+ }, [active, key]);
- }
--var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
--function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
-- const turn = location2.turn;
-- const time = location2.end.time;
-- (0, import_react6.useEffect)(() => {
-- actions.noteTurnTime(sessionId, turn, "end", time);
-- }, [sessionId, turn, time]);
-- const label = useTurnClock((state) => {
-- const start = nextTurnStart(state[sessionId] ?? {}, turn);
-- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : "";
-- });
-- const visit = useUi((state) => state.visits?.[sessionId]);
-- const fresh = useTurnClock((state) => {
-- if (typeof visit?.seen !== "number" || time > visit.seen) return false;
-- const start = nextTurnStart(state[sessionId] ?? {}, turn);
-- return start !== void 0 && start > visit.seen && start <= visit.entered;
-- });
-- if (!label && !fresh) return null;
-- return (0, import_react6.createElement)(
-- import_react6.Fragment,
-- null,
-- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
-- fresh ? (0, import_react6.createElement)(NewDivider) : null
++}
+function useNow(active) {
-+ const [now, setNow] = (0, import_react11.useState)(Date.now);
-+ (0, import_react11.useEffect)(() => {
++ const [now, setNow] = (0, import_react12.useState)(Date.now);
++ (0, import_react12.useEffect)(() => {
+ if (!active) return void 0;
+ const timer = setInterval(() => setNow(Date.now()), 3e4);
+ return () => clearInterval(timer);
@@ -8636,39 +5574,39 @@ diff --git a/client.js b/client.js
+ ...approval.roomId ? [["always-room", "Always allow for this group"]] : []
+];
+function ApprovalCard({ approval, bot, actions }) {
-+ const [busy, setBusy] = (0, import_react11.useState)(false);
++ const [busy, setBusy] = (0, import_react12.useState)(false);
+ const answer = (choice) => {
+ setBusy(true);
+ void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false));
+ };
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "div",
+ { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) },
-+ (0, import_react11.createElement)("div", { className: "bt-mw-tool" }, approval.tool),
-+ approval.detail?.text ? (0, import_react11.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null,
-+ approval.reason ? (0, import_react11.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null,
-+ approval.detail?.code ? (0, import_react11.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null,
-+ approval.answerable ? (0, import_react11.createElement)(
++ (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",
+ { className: "bt-mw-actions" },
-+ (0, import_react11.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")),
-+ (0, import_react11.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")),
-+ ...alwaysChoices(approval, bot).map(([choice, label]) => (0, import_react11.createElement)("button", { key: choice, type: "button", className: "bt-q-btn", "data-choice": choice, disabled: busy, onClick: () => answer(choice) }, t(label)))
-+ ) : (0, import_react11.createElement)(
++ (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",
+ { className: "bt-mw-actions" },
-+ (0, import_react11.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name }))
++ (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 MemberWork({ entry, bot, room: room2, actions, now }) {
-+ const [stopping, setStopping] = (0, import_react11.useState)(false);
++ const [stopping, setStopping] = (0, import_react12.useState)(false);
+ if (entry.state === "ask") {
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "div",
+ { className: "bt-mw-member" },
-+ (0, import_react11.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`),
-+ entry.approvals.map((approval) => (0, import_react11.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions }))
++ (0, import_react12.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`),
++ entry.approvals.map((approval) => (0, import_react12.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions }))
+ );
+ }
+ if (entry.state !== "work") return null;
@@ -8676,28 +5614,23 @@ diff --git a/client.js b/client.js
+ setStopping(true);
+ void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false));
+ };
-+ return (0, import_react11.createElement)(
++ return (0, import_react12.createElement)(
+ "div",
+ { className: "bt-mw-member" },
-+ (0, import_react11.createElement)(
++ (0, import_react12.createElement)(
+ "div",
+ { className: "bt-mw-state bt-mw-work" },
-+ (0, import_react11.createElement)("span", null, (0, import_react11.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)),
-+ entry.here && entry.doing.handed !== true ? (0, import_react11.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null
++ (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 Shimmer({ children }) {
-- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
++ );
++}
+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 };
- }
--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 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}
@@ -8732,23 +5665,15 @@ diff --git a/client.js b/client.js
+`;
+
+// src/client/inbox.js
-+var import_react12 = require("react");
++var import_react13 = require("react");
+var KEEP_MS = 7 * 864e5;
+var POLL_MS2 = 3e4;
+var DONE_MAX = 20;
+var FEEDBACK_MS = 5e3;
-+var line = (d) => (0, import_react12.createElement)("path", { d });
++var line = (d) => (0, import_react13.createElement)("path", { d });
+var icon = (...paths) => function StatusIcon() {
-+ return (0, import_react12.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, paths.map(line));
- };
--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"]
++ return (0, import_react13.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, paths.map(line));
++};
+var RING = "M10 3.25a6.75 6.75 0 1 1 0 13.5 6.75 6.75 0 0 1 0-13.5z";
+var InboxIcon = icon("M3.5 11.5l1.9-6.1A1.5 1.5 0 0 1 6.83 4.35h6.34a1.5 1.5 0 0 1 1.43 1.05l1.9 6.1", "M3.5 11.5v3.25a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5V11.5", "M3.5 11.5h3.75l1 1.75h3.5l1-1.75h3.75");
+var AskIcon = icon(RING, "M10 6.75v3.75", "M10 13.25h.01");
@@ -8774,69 +5699,7 @@ diff --git a/client.js b/client.js
+ { id: "needs", title: "Needs you", empty: "Nothing needs you right now." },
+ { id: "progress", title: "In progress", empty: "Nothing is running." },
+ { id: "done", title: "Done", empty: "Nothing finished in the last 7 days." }
- ];
--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 useLiveNote(actions, sessionId, id, entry) {
-- const key = entry ? JSON.stringify(entry) : "";
-- (0, import_react6.useEffect)(() => {
-- if (!key) return void 0;
-- actions.noteLive(sessionId, id, JSON.parse(key));
-- return () => actions.noteLive(sessionId, id, null);
-- }, [sessionId, id, key]);
--}
--function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) {
-- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null });
-- (0, import_react6.useEffect)(() => {
-- if (done) return void 0;
-- let stop = false;
++];
+var sharedTasks = [];
+var taskListeners = /* @__PURE__ */ new Set();
+function publishTasks(list) {
@@ -8844,209 +5707,26 @@ diff --git a/client.js b/client.js
+ for (const listener of taskListeners) listener(sharedTasks);
+}
+function useTasks(actions) {
-+ const [tasks, setTasks] = (0, import_react12.useState)(sharedTasks);
-+ (0, import_react12.useEffect)(() => {
++ const [tasks, setTasks] = (0, import_react13.useState)(sharedTasks);
++ (0, import_react13.useEffect)(() => {
+ taskListeners.add(setTasks);
+ let stopped = 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();
++ let timer;
+ const load = () => Promise.resolve(actions?.tasks?.()).then((list) => {
+ if (!stopped && list !== void 0) publishTasks(list);
+ }).catch(() => {
+ }).finally(() => {
+ if (!stopped) timer = setTimeout(load, POLL_MS2);
+ });
-+ load();
- return () => {
-- stop = true;
-+ stopped = true;
- clearTimeout(timer);
-+ taskListeners.delete(setTasks);
- };
-- }, [done, sessionId, callId]);
-- let replies = live.replies;
-- if (done) {
-- try {
-- replies = JSON.parse(contentText(content)).replies ?? [];
-- } catch {
-- 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;
-- 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)(
-- "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 }))
-- );
-- })
-- );
--}
--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;
--}
--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]);
-+ }, []);
-+ return tasks;
- }
--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;
++ load();
++ return () => {
++ stopped = true;
++ clearTimeout(timer);
++ taskListeners.delete(setTasks);
++ };
++ }, []);
++ return tasks;
++}
+var ownBot = (roster, id) => {
+ const bot = roster.byId?.[id];
+ if (bot === void 0 || bot.remote) return void 0;
@@ -9222,43 +5902,24 @@ diff --git a/client.js b/client.js
+ const text = previewOf(sessions, part);
+ if (text === "") continue;
+ done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text, place: "", chat: bot.id, ...people(roster, bot.id) });
- }
-- case "remember":
-- case "forget":
-- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null;
-- default:
-- return null;
- }
++ }
++ }
+ const newest = (a, b) => (b.at ?? Infinity) - (a.at ?? Infinity);
+ for (const card of [...needs, ...progress, ...done]) {
+ card.activity = latestActivity(card, { roster, work, sessions });
+ card.calls = toolCalls(card, { roster, work });
+ }
+ return { needs: needs.sort(newest), progress: progress.sort(newest), done: done.sort(newest).slice(0, DONE_MAX) };
- }
--function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) {
++}
+var needsCount = (roster, work, tasks) => inboxModel({ roster, work, tasks }).needs.length;
+function InboxRow({ wide, useRoster, useWork, 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 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))));
++ const roster = useRoster((value) => value);
+ const work = useWork ? useWork((value) => value) : {};
+ const open = useUi ? useUi((value) => Boolean(value.inbox)) : false;
+ const tasks = useTasks(actions);
+ const count = needsCount(roster, work, tasks);
+ const label = count > 0 ? t("Inbox, {count} need you", { count }) : t("Inbox");
-+ return (0, import_react12.createElement)(
++ return (0, import_react13.createElement)(
+ "button",
+ {
+ type: "button",
@@ -9269,14 +5930,14 @@ diff --git a/client.js b/client.js
+ "aria-expanded": open,
+ onClick: () => actions.openInbox()
+ },
-+ (0, import_react12.createElement)("span", { className: "bt-inbox-mark" }, (0, import_react12.createElement)(InboxIcon)),
-+ wide ? (0, import_react12.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null,
-+ count > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null
++ (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
+ );
+}
+function Status({ status }) {
+ const { label, tone, Icon } = STATUSES[status];
-+ return (0, import_react12.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react12.createElement)(Icon), (0, import_react12.createElement)("span", null, t(label)));
++ return (0, import_react13.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", null, t(label)));
+}
+function whenText(card, now) {
+ if (card.status === "running" || card.status === "task") {
@@ -9288,39 +5949,39 @@ diff --git a/client.js b/client.js
+ const who = card.bot ? card.bot.name : card.room?.name ?? "";
+ const meta = [who, card.place, whenText(card, now)].filter(Boolean).join(" · ");
+ const answerable = card.kind === "approval" && card.approval?.answerable === true;
-+ return (0, import_react12.createElement)(
++ return (0, import_react13.createElement)(
+ "div",
+ { className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status },
-+ (0, import_react12.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react12.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react12.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null),
-+ (0, import_react12.createElement)(
++ (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",
+ { className: "bt-inbox-copy" },
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)(
+ "span",
+ { className: "bt-inbox-top" },
-+ (0, import_react12.createElement)(Status, { status: card.status }),
-+ (0, import_react12.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta)
++ (0, import_react13.createElement)(Status, { status: card.status }),
++ (0, import_react13.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta)
+ ),
-+ (0, import_react12.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title),
-+ card.calls > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null,
-+ card.reason ? (0, import_react12.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, card.reason) : null,
-+ card.activity ? (0, import_react12.createElement)("span", { className: "bt-inbox-activity", title: card.activity }, t("Latest: {text}", { text: card.activity })) : null,
-+ card.hint ? (0, import_react12.createElement)("span", { className: "bt-inbox-hint" }, card.hint) : null,
-+ error ? (0, import_react12.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null
++ (0, import_react13.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title),
++ card.calls > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null,
++ card.reason ? (0, import_react13.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, card.reason) : null,
++ card.activity ? (0, import_react13.createElement)("span", { className: "bt-inbox-activity", title: card.activity }, t("Latest: {text}", { text: card.activity })) : null,
++ card.hint ? (0, import_react13.createElement)("span", { className: "bt-inbox-hint" }, card.hint) : null,
++ error ? (0, import_react13.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null
+ ),
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)(
+ "span",
+ { className: "bt-inbox-actions" },
-+ answerable ? (0, import_react12.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "reject") }, t("Reject")) : null,
-+ answerable ? (0, import_react12.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "allow") }, t("Allow once")) : null,
-+ ...(answerable ? alwaysChoices(card.approval, card.bot) : []).map(([choice, label]) => (0, import_react12.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_react12.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"))
++ answerable ? (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "reject") }, t("Reject")) : null,
++ answerable ? (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: () => onAnswer(card, "allow") }, t("Allow once")) : null,
++ ...(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 useNow2(ms) {
-+ const [now, setNow] = (0, import_react12.useState)(Date.now);
-+ (0, import_react12.useEffect)(() => {
++ const [now, setNow] = (0, import_react13.useState)(Date.now);
++ (0, import_react13.useEffect)(() => {
+ const timer = setInterval(() => setNow(Date.now()), ms);
+ return () => clearInterval(timer);
+ }, [ms]);
@@ -9331,13 +5992,13 @@ diff --git a/client.js b/client.js
+ const sessions = useSessions((value) => value);
+ const tasks = useTasks(actions);
+ const now = useNow2(3e4);
-+ const [answered, setAnswered] = (0, import_react12.useState)({});
-+ const [busy, setBusy] = (0, import_react12.useState)("");
-+ const [errors, setErrors] = (0, import_react12.useState)({});
-+ const [feedback, setFeedback] = (0, import_react12.useState)("");
-+ const close = (0, import_react12.useCallback)(() => actions.closeInbox(), []);
++ const [answered, setAnswered] = (0, import_react13.useState)({});
++ const [busy, setBusy] = (0, import_react13.useState)("");
++ const [errors, setErrors] = (0, import_react13.useState)({});
++ const [feedback, setFeedback] = (0, import_react13.useState)("");
++ const close = (0, import_react13.useCallback)(() => actions.closeInbox(), []);
+ useEscape(close);
-+ (0, import_react12.useEffect)(() => {
++ (0, import_react13.useEffect)(() => {
+ if (!feedback) return void 0;
+ const timer = setTimeout(() => setFeedback(""), FEEDBACK_MS);
+ return () => clearTimeout(timer);
@@ -9357,35 +6018,35 @@ diff --git a/client.js b/client.js
+ actions.closeInbox();
+ actions.openSession(card.chat);
+ };
-+ return (0, import_react12.createElement)(
++ return (0, import_react13.createElement)(
+ "div",
+ { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react12.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
++ (0, import_react13.createElement)(
+ "div",
+ { className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" },
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)(
+ "div",
+ { className: "bt-mem-head" },
-+ (0, import_react12.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")),
-+ (0, import_react12.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")),
-+ (0, import_react12.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react12.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close inbox"), autoFocus: true, onClick: close }, (0, import_react12.createElement)(CloseIcon))
++ (0, import_react13.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")),
++ (0, import_react13.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")),
++ (0, import_react13.createElement)("span", { className: "bt-mem-head-gap" }),
++ (0, import_react13.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close inbox"), autoFocus: true, onClick: close }, (0, import_react13.createElement)(CloseIcon))
+ ),
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)(
+ "div",
+ { className: "bt-mem-body bt-inbox-body" },
-+ (0, import_react12.createElement)("div", { className: "bt-inbox-feedback", role: "status", "aria-live": "polite" }, feedback),
-+ GROUPS.map((group) => (0, import_react12.createElement)(
++ (0, import_react13.createElement)("div", { className: "bt-inbox-feedback", role: "status", "aria-live": "polite" }, feedback),
++ GROUPS.map((group) => (0, import_react13.createElement)(
+ "section",
+ { key: group.id, className: "bt-inbox-group", "aria-labelledby": `bt-inbox-${group.id}` },
-+ (0, import_react12.createElement)(
++ (0, import_react13.createElement)(
+ "h3",
+ { id: `bt-inbox-${group.id}`, className: "bt-inbox-head" },
-+ (0, import_react12.createElement)("span", null, t(group.title)),
-+ groups[group.id].length > 0 ? (0, import_react12.createElement)("span", { className: "bt-inbox-head-count" }, String(groups[group.id].length)) : null
++ (0, import_react13.createElement)("span", null, t(group.title)),
++ groups[group.id].length > 0 ? (0, import_react13.createElement)("span", { className: "bt-inbox-head-count" }, String(groups[group.id].length)) : null
+ ),
-+ groups[group.id].length === 0 ? (0, import_react12.createElement)("p", { className: "bt-inbox-empty" }, t(group.empty)) : (0, import_react12.createElement)("div", { className: "bt-inbox-list" }, groups[group.id].map((card) => (0, import_react12.createElement)(InboxCard, {
++ groups[group.id].length === 0 ? (0, import_react13.createElement)("p", { className: "bt-inbox-empty" }, t(group.empty)) : (0, import_react13.createElement)("div", { className: "bt-inbox-list" }, groups[group.id].map((card) => (0, import_react13.createElement)(InboxCard, {
+ key: card.id,
+ card,
+ roster,
@@ -9399,7 +6060,7 @@ diff --git a/client.js b/client.js
+ )
+ )
+ );
- }
++}
+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)}
@@ -9437,17 +6098,7 @@ diff --git a/client.js b/client.js
+.bt-inbox-actions{flex:none;display:flex;align-items:center;gap:6px;align-self:center}
+.bt-inbox-actions .bt-send{height:28px;font-size:12px}
+`;
-
--// src/client/activity-row.js
--var import_react7 = require("react");
--var import_react_dom2 = require("react-dom");
--var PHRASE_HOLD_MS = 800;
--var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]);
--function elapsedLabel(ms) {
-- const minutes = Math.floor(ms / 6e4);
-- if (minutes < 60) return `${Math.max(1, minutes)}m`;
-- const hours = Math.floor(minutes / 60);
-- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
++
+// src/shared/task-board.js
+var COLUMNS = ["not-started", "in-progress", "done"];
+var PRIORITIES = ["urgent", "high", "normal", "low"];
@@ -9486,24 +6137,18 @@ diff --git a/client.js b/client.js
+ const value = task?.progress;
+ if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0;
+ return Math.max(0, Math.min(100, Math.round(value)));
- }
--function useRunningHost(active) {
-- const [host, setHost] = (0, import_react7.useState)(null);
-- (0, import_react7.useEffect)(() => {
-- if (!active) {
-- setHost(null);
-- return void 0;
++}
+
+// src/client/kanban.js
-+var import_react13 = require("react");
++var import_react14 = require("react");
+var POLL_MS3 = 15e3;
+function BoardIcon() {
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.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_react13.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
-+ (0, import_react13.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
-+ (0, import_react13.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 })
++ (0, import_react14.createElement)("rect", { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
++ (0, import_react14.createElement)("rect", { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
++ (0, import_react14.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 })
+ );
+}
+function shortId(id) {
@@ -9566,12 +6211,7 @@ diff --git a/client.js b/client.js
+ for (const bot of children.get(parent) ?? []) {
+ rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot });
+ walk(bot.id, depth + 1);
- }
-- let frame = 0;
-- const find = () => {
-- frame = 0;
-- const node = document.querySelector("[data-chat-flow] > [data-chat-running]");
-- setHost((prev) => prev === node ? prev : node);
++ }
+ };
+ 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 }));
@@ -9636,10 +6276,10 @@ diff --git a/client.js b/client.js
+ 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_react13.useState)([]);
-+ const [groupTasks, setGroupTasks] = (0, import_react13.useState)([]);
++ const [own, setOwn] = (0, import_react14.useState)([]);
++ const [groupTasks, setGroupTasks] = (0, import_react14.useState)([]);
+ const key = scope?.kind === "group" ? `group:${scope.roomId}` : "own";
-+ (0, import_react13.useEffect)(() => {
++ (0, import_react14.useEffect)(() => {
+ let stopped = false;
+ const load = () => {
+ const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.());
@@ -9648,22 +6288,13 @@ 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(find);
-- });
-- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
-- find();
++ };
+ load();
+ const timer = setInterval(load, POLL_MS3);
- return () => {
-- cancelAnimationFrame(frame);
-- observer.disconnect();
++ return () => {
+ stopped = true;
+ clearInterval(timer);
- };
-- }, [active]);
-- return host;
++ };
+ }, [key]);
+ const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId);
+ const replace = (task) => {
@@ -9672,44 +6303,9 @@ diff --git a/client.js b/client.js
+ else setOwn(write);
+ };
+ return { own, tasks, replace };
- }
--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 RailButton({ row, selected, meta, onSelect }) {
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "button",
+ {
+ type: "button",
@@ -9721,8 +6317,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_react13.createElement)("span", { className: "bt-board-rail-name" }, row.name),
-+ meta ? (0, import_react13.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null
++ (0, import_react14.createElement)("span", { className: "bt-board-rail-name" }, row.name),
++ meta ? (0, import_react14.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null
+ );
+}
+function BoardCard({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) {
@@ -9732,7 +6328,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_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "article",
+ {
+ className: "bt-board-card",
@@ -9748,35 +6344,35 @@ diff --git a/client.js b/client.js
+ },
+ onDragEnd: () => onDrag?.(null)
+ },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "span",
+ { className: "bt-board-card-top" },
-+ (0, import_react13.createElement)("span", { className: "bt-board-id" }, shortId(task.id)),
-+ (0, import_react13.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)),
-+ (0, import_react13.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status))
++ (0, import_react14.createElement)("span", { className: "bt-board-id" }, shortId(task.id)),
++ (0, import_react14.createElement)("span", { className: "bt-board-priority", "data-priority": priority }, priorityLabel(priority)),
++ (0, import_react14.createElement)("span", { className: "bt-board-status" }, statusLabel(task.status))
+ ),
-+ (0, import_react13.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
-+ overall ? (0, import_react13.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null,
-+ partOf2 ? (0, import_react13.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null,
-+ task.note ? (0, import_react13.createElement)("span", { className: "bt-board-note" }, task.note) : null,
-+ question ? (0, import_react13.createElement)("span", { className: "bt-board-wait" }, question) : null,
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
++ overall ? (0, import_react14.createElement)("span", { className: "bt-board-kind" }, t("Group task")) : null,
++ partOf2 ? (0, import_react14.createElement)("span", { className: "bt-board-part" }, t("Part of {title}", { title: partOf2 })) : null,
++ task.note ? (0, import_react14.createElement)("span", { className: "bt-board-note" }, task.note) : null,
++ question ? (0, import_react14.createElement)("span", { className: "bt-board-wait" }, question) : null,
++ (0, import_react14.createElement)(
+ "span",
+ { className: "bt-board-foot" },
-+ who ? (0, import_react13.createElement)("span", { className: "bt-board-who" }, who) : null,
-+ (0, import_react13.createElement)(
++ who ? (0, import_react14.createElement)("span", { className: "bt-board-who" }, who) : null,
++ (0, import_react14.createElement)(
+ "span",
+ { className: "bt-board-progress" },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "span",
+ { className: "bt-board-progress-bar", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progress, "aria-label": t("Progress") },
-+ (0, import_react13.createElement)("span", { style: { width: `${progress}%` } })
++ (0, import_react14.createElement)("span", { style: { width: `${progress}%` } })
+ ),
-+ (0, import_react13.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress }))
++ (0, import_react14.createElement)("span", { className: "bt-board-percent" }, t("{percent}%", { percent: progress }))
+ )
+ ),
-+ movable && column === "not-started" ? (0, import_react13.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_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onMove?.(task.id, "done") }, t("Complete")) : null
++ movable && column === "not-started" ? (0, import_react14.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_react14.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) {
@@ -9786,7 +6382,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_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "section",
+ {
+ className: "bt-board-column",
@@ -9801,88 +6397,50 @@ diff --git a/client.js b/client.js
+ const id = event.dataTransfer?.getData("text/plain");
+ if (id) onMove?.(id, column);
+ onDrag?.(null);
- }
-- };
-- const observer = new MutationObserver(() => {
-- if (!frame) frame = requestAnimationFrame(scan);
-- });
-- scan();
-- return () => {
-- cancelAnimationFrame(frame);
-- observer.disconnect();
-- };
-- }, []);
++ }
+ },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "h3",
+ { className: "bt-board-column-head" },
-+ (0, import_react13.createElement)("span", null, columnTitle(column)),
-+ (0, import_react13.createElement)("span", { className: "bt-board-count" }, String(tasks.length))
++ (0, import_react14.createElement)("span", null, columnTitle(column)),
++ (0, import_react14.createElement)("span", { className: "bt-board-count" }, String(tasks.length))
+ ),
-+ dragging ? (0, import_react13.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null,
-+ tasks.length === 0 && !dragging ? (0, import_react13.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null,
-+ tree ? tree.nests.map((nest) => (0, import_react13.createElement)(
++ dragging ? (0, import_react14.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null,
++ tasks.length === 0 && !dragging ? (0, import_react14.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null,
++ tree ? tree.nests.map((nest) => (0, import_react14.createElement)(
+ "div",
+ { key: nest.task.id, className: "bt-board-nest" },
-+ (0, import_react13.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)),
-+ nest.children.map((child) => (0, import_react13.createElement)(
++ (0, import_react14.createElement)(BoardCard, cardProps(nest.task, roster, onMove, onDrag)),
++ nest.children.map((child) => (0, import_react14.createElement)(
+ "div",
+ { key: child.id, className: "bt-board-subtask", "data-parent": nest.task.id },
-+ (0, import_react13.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag))
++ (0, import_react14.createElement)(BoardCard, cardProps(child, roster, onMove, onDrag))
+ ))
+ )) : null,
-+ linked.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })),
-+ lanes ? lanesOf(plain, roster).map((lane) => (0, import_react13.createElement)(
++ linked.map((task) => (0, import_react14.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "") })),
++ lanes ? lanesOf(plain, roster).map((lane) => (0, import_react14.createElement)(
+ "div",
+ { key: lane.id, className: "bt-board-lane" },
-+ (0, import_react13.createElement)("span", { className: "bt-board-lane-name" }, lane.name),
-+ lane.tasks.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
-+ )) : plain.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
++ (0, import_react14.createElement)("span", { className: "bt-board-lane-name" }, lane.name),
++ lane.tasks.map((task) => (0, import_react14.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
++ )) : plain.map((task) => (0, import_react14.createElement)(BoardCard, { key: task.id, ...cardProps(task, roster, onMove, onDrag) }))
+ );
- }
--function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) {
-- const roster = useRoster((value) => value);
-- const running = useSessionStatus((map) => map.get(sessionId)?.running === true);
-- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {}));
-- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
-- const bot = roster.byId[sessionId];
-- const room2 = roster.roomsById[sessionId];
-- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
-- const streaming = entries2.some((entry) => entry.text);
-- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at);
-- const relaying = tools.some((entry) => entry.relay);
-- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
-- const tool = tools.at(-1);
-- const inputRef = (0, import_react7.useRef)(0);
-- const inputAt = Math.max(inputRef.current, pendingInput);
-- inputRef.current = inputAt;
-- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
-- const [shown, setShown] = (0, import_react7.useState)(null);
-- const wantKey = want?.key ?? null;
-- const markState = (0, import_react7.useRef)("thinking");
-- if (want) markState.current = want.state;
-- (0, import_react7.useEffect)(() => {
-- if (wantKey === null || shown?.key === wantKey) return void 0;
-- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() });
-- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0;
-- if (wait <= 0) {
-- make();
-- return void 0;
++}
+function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) {
+ const columns = boardColumns(tasks);
+ const lanes = scope?.kind === "group";
+ const selectedBot = scope?.kind === "bot" ? scope.botId : "";
+ const selectedRoom = scope?.kind === "group" ? scope.roomId : "";
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "div",
+ { className: "bt-board-body" },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "aside",
+ { className: "bt-board-rail", "aria-label": t("Task board") },
-+ (0, import_react13.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")),
-+ rail.bots.length === 0 ? (0, import_react13.createElement)("p", { className: "bt-board-empty" }, t("No tasks yet.")) : rail.bots.map((row) => {
++ (0, import_react14.createElement)("h3", { className: "bt-board-rail-head" }, t("Your Bots")),
++ rail.bots.length === 0 ? (0, import_react14.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_react13.createElement)(RailButton, {
++ return (0, import_react14.createElement)(RailButton, {
+ key: row.id,
+ row,
+ selected: row.id === selectedBot,
@@ -9890,26 +6448,26 @@ 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_react13.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null,
-+ rail.groups.map((row) => (0, import_react13.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))
++ rail.groups.length > 0 ? (0, import_react14.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null,
++ rail.groups.map((row) => (0, import_react14.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))
+ ),
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "div",
+ { className: "bt-board-main" },
-+ error ? (0, import_react13.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null,
-+ scope?.kind === "bot" && onCreate ? (0, import_react13.createElement)(
++ error ? (0, import_react14.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null,
++ scope?.kind === "bot" && onCreate ? (0, import_react14.createElement)(
+ "form",
+ { className: "bt-board-add", onSubmit: (event) => {
+ event.preventDefault();
+ onCreate(draft);
+ } },
-+ (0, import_react13.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
-+ (0, import_react13.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
++ (0, import_react14.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
++ (0, import_react14.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
+ ) : null,
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "div",
+ { className: "bt-board-columns" },
-+ ["not-started", "in-progress", "done"].map((column) => (0, import_react13.createElement)(BoardColumn, {
++ ["not-started", "in-progress", "done"].map((column) => (0, import_react14.createElement)(BoardColumn, {
+ key: column,
+ column,
+ tasks: columns[column],
@@ -9926,7 +6484,7 @@ diff --git a/client.js b/client.js
+function KanbanRow({ wide, useUi, actions }) {
+ const open = useUi ? useUi((value) => Boolean(value.board)) : false;
+ const label = t("Tasks");
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "button",
+ {
+ type: "button",
@@ -9937,8 +6495,8 @@ diff --git a/client.js b/client.js
+ "aria-expanded": open,
+ onClick: () => actions.openBoard()
+ },
-+ (0, import_react13.createElement)("span", { className: "bt-board-mark" }, (0, import_react13.createElement)(BoardIcon)),
-+ wide ? (0, import_react13.createElement)("span", { className: "bt-board-label" }, label) : null
++ (0, import_react14.createElement)("span", { className: "bt-board-mark" }, (0, import_react14.createElement)(BoardIcon)),
++ wide ? (0, import_react14.createElement)("span", { className: "bt-board-label" }, label) : null
+ );
+}
+function scopeName(roster, scope) {
@@ -9948,12 +6506,12 @@ diff --git a/client.js b/client.js
+}
+function KanbanDialog({ roster, actions, leaving = false }) {
+ const rail = boardRail(roster);
-+ const [scope, setScope] = (0, import_react13.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_react13.useState)(null);
-+ const [error, setError] = (0, import_react13.useState)("");
-+ const [draft, setDraft] = (0, import_react13.useState)("");
++ const [scope, setScope] = (0, import_react14.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_react14.useState)(null);
++ const [error, setError] = (0, import_react14.useState)("");
++ const [draft, setDraft] = (0, import_react14.useState)("");
+ const { own, tasks, replace } = useBoardTasks(actions, scope);
-+ const close = (0, import_react13.useCallback)(() => actions.closeBoard(), []);
++ const close = (0, import_react14.useCallback)(() => actions.closeBoard(), []);
+ useEscape(close);
+ const counts2 = {};
+ for (const task of own) {
@@ -9971,19 +6529,7 @@ diff --git a/client.js b/client.js
+ replace(await actions.taskUpdate({ id, column }));
+ } catch (failure) {
+ setError(failure?.message ?? String(failure));
- }
-- 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;
++ }
+ };
+ const create = async (title) => {
+ const text = String(title ?? "").trim();
@@ -9994,68 +6540,25 @@ diff --git a/client.js b/client.js
+ setDraft("");
+ } catch (failure) {
+ setError(failure?.message ?? String(failure));
- }
-- setPhase((current) => current === "in" ? "out" : current);
-- const timer = setTimeout(() => {
-- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
-- setPhase((current) => current === "out" ? null : current);
-- }, 140);
-- return () => clearTimeout(timer);
-- }, [visible]);
-- (0, import_react7.useLayoutEffect)(() => {
-- if (phase !== null) return;
-- endedAt.current = Date.now();
-- setShown(null);
-- const before = shift.current;
-- shift.current = null;
-- const scroller = document.querySelector("[data-conversation-scroll]");
-- if (before === null || !scroller || reducedMotion()) return;
-- const delta = before - scroller.scrollTop;
-- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" });
-- }, [phase]);
-- (0, import_react7.useEffect)(() => {
-- if (!running) actions.clearLive(sessionId);
-- }, [running, sessionId]);
-- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
-- const [now, setNow] = (0, import_react7.useState)(Date.now);
-- (0, import_react7.useEffect)(() => {
-- if (phase !== "in") return void 0;
-- const timer = setInterval(() => setNow(Date.now()), 1e4);
-- return () => clearInterval(timer);
-- }, [phase]);
-- const host = useRunningHost(running);
-- if (!host || phase === null || !shown) return null;
-- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
-- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)(
++ }
+ };
+ const name = scopeName(roster, scope);
-+ return (0, import_react13.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
-- )
++ return (0, import_react14.createElement)(
++ "div",
+ { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react13.createElement)("div", { className: "bt-scrim", onClick: close }),
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)("div", { className: "bt-scrim", onClick: close }),
++ (0, import_react14.createElement)(
+ "div",
+ { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "div",
+ { className: "bt-mem-head" },
-+ (0, import_react13.createElement)("h2", { id: "bt-board-heading" }, t("Tasks")),
-+ (0, import_react13.createElement)("span", { className: "bt-mem-status" }, name),
-+ (0, import_react13.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react13.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react13.createElement)(CloseIcon))
++ (0, import_react14.createElement)("h2", { id: "bt-board-heading" }, t("Tasks")),
++ (0, import_react14.createElement)("span", { className: "bt-mem-status" }, name),
++ (0, import_react14.createElement)("span", { className: "bt-mem-head-gap" }),
++ (0, import_react14.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close tasks"), autoFocus: true, onClick: close }, (0, import_react14.createElement)(CloseIcon))
+ ),
-+ (0, import_react13.createElement)(BoardView, {
++ (0, import_react14.createElement)(BoardView, {
+ roster,
+ tasks,
+ rail,
@@ -10070,19 +6573,9 @@ diff --git a/client.js b/client.js
+ onDrag: setDragging,
+ onCreate: create
+ })
- )
-- ), host);
++ )
+ );
- }
--
--// src/client/dsh-offer.js
--var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]);
--var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0];
--var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
--function showsDshRow(dsh, app) {
-- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
-- if (dsh.kind === "npm") return true;
-- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
++}
+var KANBAN_CSS = `
+.bt-board-row{min-height:40px;padding:6px 8px;gap:10px}
+.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)}
@@ -10143,22 +6636,22 @@ diff --git a/client.js b/client.js
+ .bt-mem.bt-board{width:100vw;border-radius:0;border:0}
+ .bt-board-columns{flex-direction:column}
+ .bt-board-column{min-width:0}
- }
++}
+`;
-
++
// src/client/sidebar.js
-var import_react8 = require("react");
-+var import_react14 = require("react");
++var import_react15 = 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_react14.useEffect)(() => {
++ (0, import_react15.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_react14.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react14.createElement)(PinMarkIcon));
++var PinMark = () => (0, import_react15.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react15.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) => {
@@ -10167,7 +6660,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_react14.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
++var workLine = (lead, tool, more) => (0, import_react15.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);
@@ -10205,27 +6698,27 @@ 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_react14.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react14.createElement)(
++ return (0, import_react15.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react15.createElement)(
"button",
{
type: "button",
-@@ -4785,55 +4785,131 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
+@@ -4785,55 +8279,131 @@
onClick: () => actions.openSession(id),
onContextMenu
},
- (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_react14.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
-+ (0, import_react14.createElement)("span", { className: "bt-tile-name" }, shown),
++ (0, import_react15.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
++ (0, import_react15.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_react14.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
-+ bot.role ? (0, import_react14.createElement)("span", { className: "bt-chip" }, bot.role) : null
++ unread && !waiting && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
++ bot.role ? (0, import_react15.createElement)("span", { className: "bt-chip" }, bot.role) : null
));
}
- return (0, import_react8.createElement)(
-+ const unreadMark = wide && waiting && !active ? (0, import_react14.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
-+ const row = (0, import_react14.createElement)(
++ const unreadMark = wide && waiting && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react15.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
++ const row = (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -10242,27 +6735,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_react14.createElement)(BotAvatar, { bot, size: 36, state, main }),
-+ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
-+ wide ? (0, import_react14.createElement)(
++ (0, import_react15.createElement)(BotAvatar, { bot, size: 36, state, main }),
++ pinned && !wide ? (0, import_react15.createElement)(PinMark) : null,
++ wide ? (0, import_react15.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_react14.createElement)(
++ (0, import_react15.createElement)(
+ "span",
+ { className: "bt-row-name" },
-+ (0, import_react14.createElement)("span", { className: "bt-row-title" }, shown),
-+ bot.remote ? (0, import_react14.createElement)(RemoteMark, { remote: bot.remote }) : null,
-+ pinned ? (0, import_react14.createElement)(PinMark) : null
++ (0, import_react15.createElement)("span", { className: "bt-row-title" }, shown),
++ bot.remote ? (0, import_react15.createElement)(RemoteMark, { remote: bot.remote }) : null,
++ pinned ? (0, import_react15.createElement)(PinMark) : null
+ ),
-+ secondLine ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
++ secondLine ? (0, import_react15.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_react14.createElement)("button", {
++ const settings = wide ? (0, import_react15.createElement)("button", {
+ type: "button",
+ className: "bt-row-settings",
+ "aria-label": settingsLabel,
@@ -10273,8 +6766,8 @@ diff --git a/client.js b/client.js
+ keepSidebarStill(event.currentTarget);
+ actions.openSettings("bots", id);
+ }
-+ }, (0, import_react14.createElement)(GearIcon)) : null;
-+ if (settings) return (0, import_react14.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
++ }, (0, import_react15.createElement)(GearIcon)) : null;
++ if (settings) return (0, import_react15.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ return row;
+}
+function keepSidebarStill(node) {
@@ -10287,14 +6780,14 @@ diff --git a/client.js b/client.js
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
+}
+function RemoteMark({ remote }) {
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.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_react14.createElement)(
++ (0, import_react15.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react14.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
-+ (0, import_react14.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
++ (0, import_react15.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
++ (0, import_react15.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ )
+ );
+}
@@ -10306,15 +6799,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_react14.createElement)(
++ return (0, import_react15.createElement)(
+ "span",
+ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react14.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
-+ (0, import_react14.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
-+ (0, import_react14.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
++ (0, import_react15.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
++ (0, import_react15.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
++ (0, import_react15.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
+ )
);
}
@@ -10343,7 +6836,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_react14.createElement)(
++ return (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -10356,139 +6849,139 @@ diff --git a/client.js b/client.js
"data-room-id": id,
onClick: () => actions.openSession(id),
onContextMenu: (event) => {
-@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
+@@ -4841,20 +8411,20 @@
actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY });
}
},
- (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_react14.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
-+ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
-+ wide ? (0, import_react14.createElement)(
++ (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
++ pinned && !wide ? (0, import_react15.createElement)(PinMark) : null,
++ wide ? (0, import_react15.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_react14.createElement)("span", { className: "bt-row-name" }, (0, import_react14.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react14.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react14.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react14.createElement)(PinMark) : null),
-+ workingLine ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, preview) : null
++ (0, import_react15.createElement)("span", { className: "bt-row-name" }, (0, import_react15.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react15.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react15.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react15.createElement)(PinMark) : null),
++ workingLine ? (0, import_react15.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react15.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_react14.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
++ wide && alert && !active ? (0, import_react15.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react15.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_react14.useRef)(null);
-+ (0, import_react14.useLayoutEffect)(() => {
++ const input = (0, import_react15.useRef)(null);
++ (0, import_react15.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
-@@ -4875,10 +4875,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
+@@ -4875,10 +8445,10 @@
}
})();
};
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react8.createElement)("input", {
-+ (0, import_react14.createElement)("input", {
++ (0, import_react15.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
-@@ -4891,7 +4891,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
+@@ -4891,7 +8461,7 @@
})
);
}
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.createElement)(
"button",
{
type: "button",
-@@ -4905,8 +4905,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
+@@ -4905,8 +8475,8 @@
actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY });
}
},
- (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_react14.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
-+ (0, import_react14.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
++ (0, import_react15.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
++ (0, import_react15.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
-@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
+@@ -4941,10 +8511,10 @@
const preview = sessions[0]?.title ?? t("New session");
const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
const open = active && wide;
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
"button",
{
type: "button",
-@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
+@@ -4959,30 +8529,30 @@
actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY });
}
},
- (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_react14.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react14.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
-+ wide ? (0, import_react14.createElement)(
++ (0, import_react15.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react15.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
++ wide ? (0, import_react15.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_react14.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
-+ preview ? (0, import_react14.createElement)("span", { className: "bt-row-preview" }, preview) : null
++ (0, import_react15.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
++ preview ? (0, import_react15.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_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
++ wide && unread && !active ? (0, import_react15.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.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_react14.createElement)(PlusIcon),
-+ (0, import_react14.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
++ (0, import_react15.createElement)(PlusIcon),
++ (0, import_react15.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_react14.createElement)(AgentSessionRow, {
++ sessions.map((session, index) => (0, import_react15.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
-@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
+@@ -4996,8 +8566,8 @@
}
var GLIDE = springEasing(1e3, 63);
function useListGlide(listRef, layout, key) {
- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() });
- (0, import_react8.useLayoutEffect)(() => {
-+ const tops = (0, import_react14.useRef)({ layout, map: /* @__PURE__ */ new Map() });
-+ (0, import_react14.useLayoutEffect)(() => {
++ const tops = (0, import_react15.useRef)({ layout, map: /* @__PURE__ */ new Map() });
++ (0, import_react15.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
-@@ -5014,9 +5014,44 @@ function useListGlide(listRef, layout, key) {
+@@ -5014,9 +8584,44 @@
tops.current = { layout, map: next };
}, [layout, key]);
}
-+var NodeActions = class extends import_react14.Component {
++var NodeActions = class extends import_react15.Component {
+ constructor(props) {
+ super(props);
+ this.state = { failed: false };
@@ -10500,42 +6993,42 @@ diff --git a/client.js b/client.js
+ console.error("flybot: nodeActions failed", error);
+ }
+ render() {
-+ return this.state.failed ? null : (0, import_react14.createElement)(this.props.component, { bot: this.props.bot });
++ return this.state.failed ? null : (0, import_react15.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_react14.createElement)(
-+ import_react14.Fragment,
++ return (0, import_react15.createElement)(
++ import_react15.Fragment,
+ null,
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
+ "div",
+ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
-+ children.length ? (0, import_react14.createElement)(
++ children.length ? (0, import_react15.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_react14.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
-+ ) : (0, import_react14.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
-+ (0, import_react14.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
-+ typeof actions === "function" ? (0, import_react14.createElement)(NodeActions, { component: actions, bot }) : null
++ (0, import_react15.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
++ ) : (0, import_react15.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
++ (0, import_react15.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
++ typeof actions === "function" ? (0, import_react15.createElement)(NodeActions, { component: actions, bot }) : null
+ ),
-+ closed ? null : children.map((child) => (0, import_react14.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
++ closed ? null : children.map((child) => (0, import_react15.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_react14.useRef)(null);
++ const listRef = (0, import_react15.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
-@@ -5025,17 +5025,26 @@ function TeamSidebar(props) {
+@@ -5025,17 +8630,26 @@
for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0;
return JSON.stringify(stamps);
});
- const order = (0, import_react8.useMemo)(() => {
-+ const order = (0, import_react14.useMemo)(() => {
++ const order = (0, import_react15.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 })),
@@ -10553,7 +7046,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_react14.useState)({});
++ const [collapsed, setCollapsed] = (0, import_react15.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;
@@ -10561,16 +7054,16 @@ diff --git a/client.js b/client.js
useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(","));
useLandOnMainBot(roster, useSessions, actions);
const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide };
-@@ -5045,39 +5045,42 @@ function TeamSidebar(props) {
+@@ -5045,39 +8659,42 @@
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react8.createElement)(
-+ wide ? (0, import_react14.createElement)(
++ wide ? (0, import_react15.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)),
@@ -10579,31 +7072,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_react14.createElement)("span", { className: "bt-brand" }, (0, import_react14.createElement)(BrandMark, { size: 22 }), (0, import_react14.createElement)(BrandName)),
-+ (0, import_react14.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react14.createElement)(SearchIcon)),
-+ (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react14.createElement)(PlusIcon))
-+ ) : (0, import_react14.createElement)(
-+ import_react14.Fragment,
++ (0, import_react15.createElement)("span", { className: "bt-brand" }, (0, import_react15.createElement)(BrandMark, { size: 22 }), (0, import_react15.createElement)(BrandName)),
++ (0, import_react15.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
++ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react15.createElement)(SearchIcon)),
++ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react15.createElement)(PlusIcon))
++ ) : (0, import_react15.createElement)(
++ import_react15.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_react14.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react14.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react14.createElement)(PlusIcon))
++ (0, import_react15.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
++ (0, import_react15.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react15.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_react14.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
-+ (0, import_react14.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
-+ (0, import_react14.createElement)(KanbanRow, { wide, useUi, actions }),
-+ (0, import_react14.createElement)(
++ wide && roster.readOnly ? (0, import_react15.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
++ (0, import_react15.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
++ (0, import_react15.createElement)(KanbanRow, { wide, useUi, actions }),
++ (0, import_react15.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_react14.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react14.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react14.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react14.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_react14.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react14.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react14.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
-+ wide ? (0, import_react14.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react14.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
++ tree ? (0, import_react15.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react15.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react15.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react15.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_react15.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react15.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react15.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
++ wide ? (0, import_react15.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react15.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
)
);
}
@@ -10615,16 +7108,16 @@ 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_react14.useState)(false);
-+ const [box, setBox] = (0, import_react14.useState)(null);
-+ const [error, setError] = (0, import_react14.useState)("");
-+ const [dshError, setDshError] = (0, import_react14.useState)("");
-+ const button = (0, import_react14.useRef)(null);
-+ (0, import_react14.useEffect)(() => {
++ const [open, setOpen] = (0, import_react15.useState)(false);
++ const [box, setBox] = (0, import_react15.useState)(null);
++ const [error, setError] = (0, import_react15.useState)("");
++ const [dshError, setDshError] = (0, import_react15.useState)("");
++ const button = (0, import_react15.useRef)(null);
++ (0, import_react15.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return;
-@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -5116,14 +8733,14 @@
setDshError("");
void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure))));
};
@@ -10636,73 +7129,73 @@ 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_react14.createElement)(
-+ import_react14.Fragment,
++ return (0, import_react15.createElement)(
++ import_react15.Fragment,
null,
- (0, import_react8.createElement)("button", {
-+ (0, import_react14.createElement)("button", {
++ (0, import_react15.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
-@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -5133,21 +8750,21 @@
"aria-haspopup": "dialog",
"aria-expanded": open,
onClick: toggle
- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
-+ }, (0, import_react14.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react14.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
-+ open && box ? (0, import_react_dom3.createPortal)((0, import_react14.createElement)(
++ }, (0, import_react15.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react15.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
++ open && box ? (0, import_react_dom3.createPortal)((0, import_react15.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_react14.createElement)(
-+ import_react14.Fragment,
++ update?.available || phase !== "idle" ? (0, import_react15.createElement)(
++ import_react15.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_react14.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
-+ (0, import_react14.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_react14.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
++ (0, import_react15.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_react15.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
++ (0, import_react15.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_react14.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
-+ phase === "installed" ? null : (0, import_react14.createElement)("button", {
++ update.notesUrl ? (0, import_react15.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
++ phase === "installed" ? null : (0, import_react15.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
-@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -5155,17 +8772,17 @@
}, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
)
) : null,
- dshRow ? (0, import_react8.createElement)(
-+ dshRow ? (0, import_react14.createElement)(
++ dshRow ? (0, import_react15.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_react14.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
-+ (0, import_react14.createElement)("div", { className: "bt-update-text" }, dshText),
-+ dshMessage ? (0, import_react14.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
++ (0, import_react15.createElement)("div", { className: "bt-update-text" }, dshText),
++ dshMessage ? (0, import_react15.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
++ (0, import_react15.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_react14.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_react14.createElement)("button", {
++ dsh.kind === "app" ? dsh.available ? (0, import_react15.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_react15.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
-@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -5176,61 +8793,25 @@
), document.body) : null
);
}
@@ -10730,11 +7223,11 @@ diff --git a/client.js b/client.js
- setTip(false);
- };
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
"button",
- {
- ref: button,
@@ -10757,31 +7250,31 @@ 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_react14.createElement)(PlugIcon),
++ (0, import_react15.createElement)(PlugIcon),
+ t("Connect apps")
+ )
);
}
function YouAvatar() {
- return (0, import_react8.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.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_react14.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react14.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
-+ (0, import_react14.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
++ (0, import_react15.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react15.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
++ (0, import_react15.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
);
}
var openShellSettings = null;
-@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) {
+@@ -5259,6 +8840,18 @@
else openHarnessSettings();
});
}
+var MODELS_STEP_ID = "flybot-models";
+var MODELS_STEP_ORDER = 1e3;
+function ModelsSettingsStep({ explicit, complete, openSection }) {
-+ (0, import_react14.useLayoutEffect)(() => {
++ (0, import_react15.useLayoutEffect)(() => {
+ if (explicit) openSection("models");
+ complete();
+ }, []);
@@ -10793,14 +7286,14 @@ diff --git a/client.js b/client.js
function openThroughMenu(trigger) {
const before = new Set(document.querySelectorAll('[role="menu"]'));
trigger.click();
-@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() {
+@@ -5288,32 +8881,32 @@
}));
}
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_react14.useState)(false);
-+ (0, import_react14.useLayoutEffect)(() => {
++ const [open, setOpen] = (0, import_react15.useState)(false);
++ (0, import_react15.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
@@ -10808,13 +7301,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_react14.useState)(null);
-+ const button = (0, import_react14.useRef)(null);
-+ (0, import_react14.useEffect)(() => {
++ const [box, setBox] = (0, import_react15.useState)(null);
++ const button = (0, import_react15.useRef)(null);
++ (0, import_react15.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react14.useEffect)(() => {
++ (0, import_react15.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
@@ -10822,7 +7315,7 @@ diff --git a/client.js b/client.js
};
}, [openSettings]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react14.useEffect)(() => {
++ (0, import_react15.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
@@ -10830,11 +7323,11 @@ diff --git a/client.js b/client.js
};
}, [openOnboarding]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react14.useEffect)(() => {
++ (0, import_react15.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return;
-@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
+@@ -5341,30 +8934,32 @@
const groups = [
[
[t("Usage"), GaugeIcon, () => actions.openSettings("usage")],
@@ -10852,54 +7345,54 @@ diff --git a/client.js b/client.js
];
- return (0, import_react8.createElement)(
- import_react8.Fragment,
-+ return (0, import_react14.createElement)(
-+ import_react14.Fragment,
++ return (0, import_react15.createElement)(
++ import_react15.Fragment,
null,
- (0, import_react8.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.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_react14.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react14.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
++ (0, import_react15.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react15.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_react14.createElement)(
++ open && box ? (0, import_react_dom3.createPortal)((0, import_react15.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_react14.createElement)(FlyBotAccountHeader, { actions }),
-+ groups.map((group, index) => (0, import_react14.createElement)(
-+ import_react14.Fragment,
++ (0, import_react15.createElement)(FlyBotAccountHeader, { actions }),
++ groups.map((group, index) => (0, import_react15.createElement)(
++ import_react15.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_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, hint]) => (0, import_react14.createElement)("button", {
++ index > 0 ? (0, import_react15.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, hint]) => (0, import_react15.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
-@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
+@@ -5372,7 +8967,7 @@
setOpen(false);
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
-+ }, (0, import_react14.createElement)(Icon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react14.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
++ }, (0, import_react15.createElement)(Icon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react15.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
))
), document.body) : null
);
-@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) {
+@@ -5382,8 +8977,8 @@
const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0);
return row ? `${row.id}|${row.blank ? 1 : 0}` : "";
});
- const lastBot = (0, import_react8.useRef)(null);
- (0, import_react8.useEffect)(() => {
-+ const lastBot = (0, import_react14.useRef)(null);
-+ (0, import_react14.useEffect)(() => {
++ const lastBot = (0, import_react15.useRef)(null);
++ (0, import_react15.useEffect)(() => {
if (!roster.ready || !roster.mainBotId) return void 0;
const [id, blank3] = mainView.split("|");
const bot = id === "" ? void 0 : roster.byId[id];
-@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) {
+@@ -5397,40 +8992,40 @@
return void 0;
}
const last = lastBot.current;
@@ -10912,20 +7405,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_react14.createElement)("span", { className: "bt-foot-account" }, (0, import_react14.createElement)(AccountLauncher, { ...props, seat: false }));
++ return (0, import_react15.createElement)("span", { className: "bt-foot-account" }, (0, import_react15.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_react14.createElement)(
++ return (0, import_react15.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_react14.createElement)(FlyBotMark, { size: 20 }),
-+ wide ? (0, import_react14.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
++ (0, import_react15.createElement)(FlyBotMark, { size: 20 }),
++ wide ? (0, import_react15.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -10942,15 +7435,15 @@ 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_react14.createElement)("span", { className: "bt-brand-mark" }, (0, import_react14.createElement)(FlyBotMark, { size }));
++ return (0, import_react15.createElement)("span", { className: "bt-brand-mark" }, (0, import_react15.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot");
-+ return (0, import_react14.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
++ return (0, import_react15.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
}
var SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
-@@ -5491,18 +5491,28 @@ var SIDEBAR_CSS = `
+@@ -5491,18 +9086,28 @@
.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)}
@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}}
@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}}
@@ -10968,53 +7461,53 @@ diff --git a/client.js b/client.js
// src/client/header.js
-var import_react9 = require("react");
-+var import_react15 = require("react");
++var import_react16 = 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_react15.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_react15.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" }));
++var GoArrow = () => (0, import_react16.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_react16.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_react15.useState)(false);
-+ (0, import_react15.useEffect)(() => {
++ const [shown, setShown] = (0, import_react16.useState)(false);
++ (0, import_react16.useEffect)(() => {
const left = seen + MEMORY_NOTICE_MS - Date.now();
setShown(seen > 0 && left > 0);
if (seen === 0 || left <= 0) return void 0;
-@@ -5512,16 +5512,16 @@ function useMemoryNotice(ids, useMemoryNews) {
+@@ -5512,16 +9117,16 @@
return shown;
}
function MemoryNotice({ shown }) {
- const inner = (0, import_react9.useRef)(null);
- const [width, setWidth] = (0, import_react9.useState)(0);
-+ const inner = (0, import_react15.useRef)(null);
-+ const [width, setWidth] = (0, import_react15.useState)(0);
++ const inner = (0, import_react16.useRef)(null);
++ const [width, setWidth] = (0, import_react16.useState)(0);
const label = t("Memory updated");
- (0, import_react9.useLayoutEffect)(() => {
-+ (0, import_react15.useLayoutEffect)(() => {
++ (0, import_react16.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react9.createElement)(
-+ return (0, import_react15.createElement)(
++ return (0, import_react16.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_react15.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react15.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react15.createElement)(MemoryIcon)), label)
++ (0, import_react16.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react16.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react16.createElement)(MemoryIcon)), label)
);
}
function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
-@@ -5537,7 +5537,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
+@@ -5537,7 +9142,7 @@
const first = openingTurn(state[sessionId], firstTurns, hiddenTurns);
return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : "";
});
- (0, import_react9.useEffect)(() => {
-+ (0, import_react15.useEffect)(() => {
++ (0, import_react16.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");
-@@ -5548,52 +5548,40 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
+@@ -5548,52 +9153,40 @@
const bot = roster.byId[sessionId];
const room2 = roster.roomsById[sessionId];
const detailsOpen = useUi((state) => state.details === sessionId);
@@ -11044,11 +7537,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_react15.createElement)(
++ return (0, import_react16.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react9.createElement)(
-+ (0, import_react15.createElement)(
++ (0, import_react16.createElement)(
"button",
{
type: "button",
@@ -11064,28 +7557,29 @@ 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_react15.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
-+ (0, import_react15.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_react15.createElement)(MemoryNotice, { shown: memoryNotice }),
-+ (0, import_react15.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react15.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_react15.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
-+ (0, import_react15.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
-+ (0, import_react15.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })
++ bot ? (0, import_react16.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
++ (0, import_react16.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_react16.createElement)(MemoryNotice, { shown: memoryNotice }),
++ (0, import_react16.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react16.createElement)(GoArrow))
++ ),
++ (0, import_react16.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
++ (0, import_react16.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
++ (0, import_react16.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_react15.useState)(null);
-+ (0, import_react15.useEffect)(() => {
++ const [view, setView] = (0, import_react16.useState)(null);
++ (0, import_react16.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
-@@ -5642,7 +5642,7 @@ function useTranscriptView() {
+@@ -5642,7 +9235,7 @@
}, []);
return view;
}
@@ -11094,76 +7588,76 @@ diff --git a/client.js b/client.js
const replies = useSessions((list) => {
const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline;
return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0;
-@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
+@@ -5658,18 +9251,22 @@
});
const view = useTranscriptView();
const unpinned = view?.unpinned === true;
- 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_react15.useState)({ base: null, dismissed: false });
-+ const [above, setAbove] = (0, import_react15.useState)({ seen: false, dismissed: false });
-+ (0, import_react15.useEffect)(() => {
++ const [release, setRelease] = (0, import_react16.useState)({ base: null, dismissed: false });
++ const [above, setAbove] = (0, import_react16.useState)({ seen: false, dismissed: false });
++ (0, import_react16.useEffect)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react9.useEffect)(() => {
-+ (0, import_react15.useEffect)(() => {
++ (0, import_react16.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_react15.useEffect)(() => {
++ (0, import_react16.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_react15.useEffect)(() => {
++ (0, import_react16.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
-@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
+@@ -5677,16 +9274,16 @@
}, [showDown]);
if (!view || !showDown && !showUp) return null;
const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages");
- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)(
-+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react15.createElement)(
++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react16.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react9.createElement)(
-+ (0, import_react15.createElement)(
++ (0, import_react16.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_react15.createElement)("span", { className: "bt-news-ico" }, (0, import_react15.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
++ (0, import_react16.createElement)("span", { className: "bt-news-ico" }, (0, import_react16.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react9.createElement)("button", {
-+ (0, import_react15.createElement)("button", {
++ (0, import_react16.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
-@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
+@@ -5694,7 +9291,7 @@
event.stopPropagation();
onDismiss();
}
- }, (0, import_react9.createElement)(CloseIcon))
-+ }, (0, import_react15.createElement)(CloseIcon))
++ }, (0, import_react16.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
-@@ -5704,8 +5704,8 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
+@@ -5704,8 +9301,8 @@
};
const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" });
return (0, import_react_dom4.createPortal)(
- (0, import_react9.createElement)(
- import_react9.Fragment,
-+ (0, import_react15.createElement)(
-+ import_react15.Fragment,
++ (0, import_react16.createElement)(
++ import_react16.Fragment,
null,
showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null,
showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null
-@@ -5714,19 +5714,117 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
+@@ -5714,19 +9311,117 @@
);
}
@@ -11267,7 +7761,7 @@ diff --git a/client.js b/client.js
+
// src/client/composer.js
-var import_react10 = require("react");
-+var import_react16 = require("react");
++var import_react17 = require("react");
var import_react_dom5 = require("react-dom");
var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition;
var keepComposerFocus = (event) => event.preventDefault();
@@ -11276,19 +7770,19 @@ 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_react16.useState)(false);
-+ const [note, setNote] = (0, import_react16.useState)("");
-+ const recognition = (0, import_react16.useRef)(null);
-+ const latest = (0, import_react16.useRef)(onText);
++ const [listening, setListening] = (0, import_react17.useState)(false);
++ const [note, setNote] = (0, import_react17.useState)("");
++ const recognition = (0, import_react17.useRef)(null);
++ const latest = (0, import_react17.useRef)(onText);
latest.current = onText;
- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react10.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => () => recognition.current?.abort(), []);
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.useEffect)(() => () => recognition.current?.abort(), []);
++ (0, import_react17.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
-@@ -5774,19 +5774,38 @@ function useDictation(onText) {
+@@ -5774,19 +9469,38 @@
const stop = () => recognition.current?.stop();
return { listening, note, start, stop };
}
@@ -11298,14 +7792,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_react16.createElement)(
++ return (0, import_react17.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_react16.createElement)(
++ return (0, import_react17.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 })
@@ -11317,7 +7811,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_react16.useEffect)(() => {
++ (0, import_react17.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -11330,28 +7824,28 @@ diff --git a/client.js b/client.js
const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]');
if (box instanceof HTMLElement) {
box.focus();
-@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
+@@ -5797,10 +9511,10 @@
}, [quote?.token]);
if (sessionId === void 0) return null;
const pluginMic = actions.speech() !== void 0;
- return (0, import_react10.createElement)(
- import_react10.Fragment,
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- pluginMic ? null : (0, import_react10.createElement)("button", {
-+ pluginMic ? null : (0, import_react16.createElement)("button", {
++ pluginMic ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
-@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
+@@ -5808,16 +9522,16 @@
title: dictation.listening ? t("Stop dictation") : t("Dictate"),
onMouseDown: keepComposerFocus,
onClick: () => dictation.listening ? dictation.stop() : dictation.start()
- }, (0, import_react10.createElement)(MicIcon)),
- (0, import_react10.createElement)("button", {
-+ }, (0, import_react16.createElement)(MicIcon)),
-+ (0, import_react16.createElement)("button", {
++ }, (0, import_react17.createElement)(MicIcon)),
++ (0, import_react17.createElement)("button", {
type: "button",
className: "bt-voice",
"aria-label": t("Start voice chat"),
@@ -11360,73 +7854,73 @@ 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_react16.createElement)(WaveIcon, { size: 18 })),
-+ dictation.note ? (0, import_react16.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
++ }, (0, import_react17.createElement)(WaveIcon, { size: 18 })),
++ dictation.note ? (0, import_react17.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
);
}
function menuIcons(roster) {
-@@ -5825,17 +5825,17 @@ function menuIcons(roster) {
+@@ -5825,17 +9539,17 @@
return (row) => {
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_react16.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
++ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react17.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_react16.useState)(null);
++ const [open, setOpen] = (0, import_react17.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_react16.useRef)(null);
-+ const menu = (0, import_react16.useRef)(null);
-+ (0, import_react16.useEffect)(() => {
++ const button = (0, import_react17.useRef)(null);
++ const menu = (0, import_react17.useRef)(null);
++ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null);
-@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
+@@ -5876,10 +9590,10 @@
actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
};
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- return (0, import_react10.createElement)(
- import_react10.Fragment,
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react10.createElement)("button", {
-+ (0, import_react16.createElement)("button", {
++ (0, import_react17.createElement)("button", {
ref: button,
type: "button",
className: "bt-plus",
-@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
+@@ -5889,8 +9603,8 @@
"aria-expanded": Boolean(open),
onMouseDown: keepComposerFocus,
onClick: toggle
- }, (0, import_react10.createElement)(PlusIcon)),
- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)(
-+ }, (0, import_react16.createElement)(PlusIcon)),
-+ shown ? (0, import_react_dom5.createPortal)((0, import_react16.createElement)(
++ }, (0, import_react17.createElement)(PlusIcon)),
++ shown ? (0, import_react_dom5.createPortal)((0, import_react17.createElement)(
"div",
{
ref: menu,
-@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
+@@ -5900,9 +9614,9 @@
style: { left: shown.left, bottom: shown.bottom },
"data-leaving": leaving || void 0
},
- (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_react16.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react16.createElement)(PaperclipIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
-+ canReference ? (0, import_react16.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react16.createElement)(ChatsIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
-+ (0, import_react16.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react16.createElement)(ClockIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
++ (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react17.createElement)(PaperclipIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
++ canReference ? (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react17.createElement)(ChatsIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
++ (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react17.createElement)(ClockIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
), document.body) : null
);
}
-@@ -5922,15 +5922,55 @@ var conversationOf = (roster, sessionId) => {
+@@ -5922,15 +9636,55 @@
function mentionRows(roster, sessionId, query = "") {
const here = conversationOf(roster, sessionId);
if (here === null) return null;
@@ -11486,7 +7980,7 @@ diff --git a/client.js b/client.js
function chatRows(roster, list, sessionId) {
const here = conversationOf(roster, sessionId);
if (here === null) return [];
-@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) {
+@@ -5959,14 +9713,31 @@
order: -20,
showGroupTitle: false,
async candidates(session, { query }) {
@@ -11522,7 +8016,7 @@ diff --git a/client.js b/client.js
},
onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0
};
-@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) {
+@@ -5995,6 +9766,83 @@
return [bots, chats];
}
@@ -11606,48 +8100,10 @@ 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"]);
-@@ -6083,270 +6083,46 @@ async function openCapture() {
- recorder.onstop = resolve;
- recorder.stop();
- });
-- stream.getTracks().forEach((track) => track.stop());
-- const blob = new Blob(chunks, { type: recorder.mimeType });
-- if (blob.size === 0) return null;
-- const decoded = await audio.decodeAudioData(await blob.arrayBuffer());
-- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3));
-- const offline = new OfflineAudioContext(1, length, 16e3);
-- const source = offline.createBufferSource();
-- source.buffer = decoded;
-- source.connect(offline.destination);
-- source.start();
-- return waveBytes((await offline.startRendering()).getChannelData(0));
-- } finally {
-- close();
-- }
-- }
-- };
--}
--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";
-- };
--}
--
+@@ -6121,232 +9969,8 @@
+ };
+ }
+
-// src/client/overlay-hooks.js
-var import_react11 = require("react");
-var escapeLayers = [];
@@ -11870,51 +8326,15 @@ diff --git a/client.js b/client.js
- )
- )
- );
-+ stream.getTracks().forEach((track) => track.stop());
-+ const blob = new Blob(chunks, { type: recorder.mimeType });
-+ if (blob.size === 0) return null;
-+ const decoded = await audio.decodeAudioData(await blob.arrayBuffer());
-+ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3));
-+ const offline = new OfflineAudioContext(1, length, 16e3);
-+ const source = offline.createBufferSource();
-+ source.buffer = decoded;
-+ source.connect(offline.destination);
-+ source.start();
-+ return waveBytes((await offline.startRendering()).getChannelData(0));
-+ } finally {
-+ close();
-+ }
-+ }
-+ };
-+}
-+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_react17 = require("react");
++var import_react18 = require("react");
var import_react_dom6 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
-@@ -6367,17 +6367,10 @@ function groupModels(models) {
+@@ -6367,17 +9991,10 @@
});
}
var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref);
@@ -11929,83 +8349,83 @@ 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_react17.useState)(void 0);
-+ (0, import_react17.useEffect)(() => {
++ const [fetched, setFetched] = (0, import_react18.useState)(void 0);
++ (0, import_react18.useEffect)(() => {
setFetched(void 0);
if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
let live = true;
-@@ -6402,12 +6402,12 @@ function effortRows(reasoning) {
+@@ -6402,12 +10019,12 @@
];
}
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react17.createElement)(
-+ import_react17.Fragment,
++ return (0, import_react18.createElement)(
++ import_react18.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_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
-+ effortRows(reasoning).map((row) => (0, import_react17.createElement)(
++ (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
++ effortRows(reasoning).map((row) => (0, import_react18.createElement)(
"button",
{
key: row.effort ?? "default",
-@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) {
+@@ -6416,17 +10033,17 @@
"aria-checked": row.effort === current,
onClick: () => onPick(row.effort)
},
- (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_react17.createElement)("span", { className: "bt-menu-label" }, row.label),
-+ (0, import_react17.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react17.createElement)(CheckIcon) : null)
++ (0, import_react18.createElement)("span", { className: "bt-menu-label" }, row.label),
++ (0, import_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react18.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_react17.createElement)(
-+ import_react17.Fragment,
++ return groupModels(roster.models).map(([provider, list]) => (0, import_react18.createElement)(
++ import_react18.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_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
-+ list.map((model) => (0, import_react17.createElement)(
++ (0, import_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
++ list.map((model) => (0, import_react18.createElement)(
"button",
{
key: model.ref,
-@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) {
+@@ -6436,20 +10053,20 @@
title: model.ref,
onClick: () => onPick(model.ref)
},
- (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_react17.createElement)("span", { className: "bt-menu-label" }, model.name),
-+ model.ref === roster.defaultModel ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
-+ (0, import_react17.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react17.createElement)(CheckIcon) : null)
++ (0, import_react18.createElement)("span", { className: "bt-menu-label" }, model.name),
++ model.ref === roster.defaultModel ? (0, import_react18.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
++ (0, import_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react18.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_react17.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_react17.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
++var Caret = () => (0, import_react18.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_react18.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_react17.useState)(null);
++ const [place, setPlace] = (0, import_react18.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_react17.useRef)(null);
-+ const menu = (0, import_react17.useRef)(null);
++ const button = (0, import_react18.useRef)(null);
++ const menu = (0, import_react18.useRef)(null);
const close = () => setPlace(null);
- (0, import_react13.useEffect)(() => {
-+ (0, import_react17.useEffect)(() => {
++ (0, import_react18.useEffect)(() => {
if (place === null) return void 0;
const onDown = (event) => {
if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return;
-@@ -6484,6 +6484,7 @@ function useAnchoredMenu() {
+@@ -6484,6 +10101,7 @@
return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close };
}
function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) {
@@ -12013,53 +8433,53 @@ diff --git a/client.js b/client.js
const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu();
const shown = modelOf(bot, roster);
const current = bot.model ?? bot.appliedModel;
-@@ -6511,10 +6511,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo
+@@ -6511,10 +10129,10 @@
else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null }));
};
const models = roster.models ?? [];
- return (0, import_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react17.createElement)(
-+ import_react17.Fragment,
++ return (0, import_react18.createElement)(
++ import_react18.Fragment,
null,
- (0, import_react13.createElement)(
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
"button",
{
ref: button,
-@@ -6527,37 +6527,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo
+@@ -6527,37 +10145,38 @@
"data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0,
onClick: toggle
},
- (0, import_react13.createElement)(
-+ (0, import_react17.createElement)(
++ (0, import_react18.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_react17.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
-+ shown ? (0, import_react17.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_react18.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
++ shown ? (0, import_react18.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_react17.createElement)(Caret)
++ (0, import_react18.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)(
-+ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react17.createElement)(
++ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react18.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_react17.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react17.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
-+ effortRows(reasoning).length > 0 ? (0, import_react17.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
-+ (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react17.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
++ models.length > 0 ? (0, import_react18.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react18.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
++ effortRows(reasoning).length > 0 ? (0, import_react18.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
++ (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react18.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_react17.createElement)(SlidersIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
++ } }, (0, import_react18.createElement)(SlidersIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
), document.body) : null
);
}
@@ -12068,89 +8488,89 @@ 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_react17.useState)("");
-+ const [error, setError] = (0, import_react17.useState)("");
++ const [busy, setBusy] = (0, import_react18.useState)("");
++ const [error, setError] = (0, import_react18.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react13.useEffect)(() => {
-+ (0, import_react17.useEffect)(() => {
++ (0, import_react18.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_react17.createElement)(RemoteGoneDock, { bot }) : null;
++ if (bot.remote) return bot.remote.gone ? (0, import_react18.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;
-@@ -6570,12 +6570,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
+@@ -6570,12 +10189,12 @@
}).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_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_react17.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react17.createElement)(KeyHelpLink, { key: "key-help" })] : [
-+ ...groupModels(models).map(([provider, list]) => (0, import_react17.createElement)(
++ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react18.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react18.createElement)(KeyHelpLink, { key: "key-help" })] : [
++ ...groupModels(models).map(([provider, list]) => (0, import_react18.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_react17.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
-+ (0, import_react17.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react17.createElement)("button", {
++ (0, import_react18.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
++ (0, import_react18.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react18.createElement)("button", {
key: model.ref,
type: "button",
className: "bt-model-chip",
-@@ -6584,50 +6584,115 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
+@@ -6584,50 +10203,115 @@
"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_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
-+ }, model.name, busy === model.ref ? (0, import_react17.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
++ }, model.name, busy === model.ref ? (0, import_react18.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_react17.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react17.createElement)(SlidersIcon), t("Manage models"))
++ (0, import_react18.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react18.createElement)(SlidersIcon), t("Manage models"))
];
- return (0, import_react13.createElement)(
-+ return (0, import_react17.createElement)(
++ return (0, import_react18.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react13.createElement)(
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react13.createElement)(
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react13.createElement)(
-+ (0, import_react17.createElement)(
++ (0, import_react18.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_react17.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react17.createElement)("div", { className: "bt-bubble" }, text)
++ (0, import_react18.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react18.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_react17.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
-+ error ? (0, import_react17.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
++ (0, import_react18.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
++ error ? (0, import_react18.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
+ );
+}
+function RemoteGoneDock({ bot }) {
-+ return (0, import_react17.createElement)(
++ return (0, import_react18.createElement)(
+ "div",
+ { className: "bt-model-dock", "data-flybot": "remote-gone" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-astack", "data-lead": "" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-msg" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-msg-line" },
-+ (0, import_react17.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react17.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_react18.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react18.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 }))
+ )
+ )
+ )
@@ -12166,7 +8586,7 @@ diff --git a/client.js b/client.js
}
+var KEY_HELP_URL = "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/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md";
+function KeyHelpLink() {
-+ return (0, import_react17.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_react18.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 ?? "");
@@ -12190,22 +8610,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_react17.createElement)(
++ return (0, import_react18.createElement)(
+ "div",
+ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
-+ (0, import_react17.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-turn-error-copy" },
-+ (0, import_react17.createElement)("span", { className: "bt-turn-error-title" }, title),
-+ (0, import_react17.createElement)("span", { className: "bt-turn-error-message" }, message),
-+ code ? (0, import_react17.createElement)("code", { className: "bt-turn-error-code" }, code) : null
++ (0, import_react18.createElement)("span", { className: "bt-turn-error-title" }, title),
++ (0, import_react18.createElement)("span", { className: "bt-turn-error-message" }, message),
++ code ? (0, import_react18.createElement)("code", { className: "bt-turn-error-code" }, code) : null
+ ),
-+ choices.length > 0 ? (0, import_react17.createElement)(
++ choices.length > 0 ? (0, import_react18.createElement)(
+ "div",
+ { className: "bt-turn-error-actions" },
-+ choices.map(([label, run]) => (0, import_react17.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
-+ deepseek ? (0, import_react17.createElement)(KeyHelpLink, { key: "key-help" }) : null
++ choices.map(([label, run]) => (0, import_react18.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
++ deepseek ? (0, import_react18.createElement)(KeyHelpLink, { key: "key-help" }) : null
+ ) : null
+ );
+}
@@ -12214,22 +8634,22 @@ 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_react17.createElement)(SignInHost, { key: open, entry, actions });
++ return (0, import_react18.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {});
-+ const [ops] = (0, import_react17.useState)(() => entry.inject?.() ?? {});
++ const [ops] = (0, import_react18.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_react17.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
-+ const [hooks] = (0, import_react17.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
-+ return (0, import_react17.createElement)(entry.component, {
++ const useHook = (source) => (selector) => (0, import_react18.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
++ const [hooks] = (0, import_react18.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
++ return (0, import_react18.createElement)(entry.component, {
...ops,
...hooks,
t: actions.accountT(),
-@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) {
+@@ -6639,6 +10323,16 @@
});
}
var MODEL_CSS = `
@@ -12246,12 +8666,12 @@ diff --git a/client.js b/client.js
.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease}
.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)}
.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
-@@ -6675,18 +6675,88 @@ var MODEL_CSS = `
+@@ -6675,18 +10369,88 @@
@media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}}
`;
+// src/client/model-test.js
-+var import_react18 = require("react");
++var import_react19 = 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.",
@@ -12276,12 +8696,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_react18.useState)({ busy: false, outcome: void 0 });
-+ const live = (0, import_react18.useRef)(true);
-+ (0, import_react18.useEffect)(() => () => {
++ const [state, setState] = (0, import_react19.useState)({ busy: false, outcome: void 0 });
++ const live = (0, import_react19.useRef)(true);
++ (0, import_react19.useEffect)(() => () => {
+ live.current = false;
+ }, []);
-+ (0, import_react18.useEffect)(() => {
++ (0, import_react19.useEffect)(() => {
+ setState({ busy: false, outcome: void 0 });
+ }, [bot.id, bot.model]);
+ const start = () => {
@@ -12295,21 +8715,21 @@ diff --git a/client.js b/client.js
+ });
+ };
+ const { busy, outcome } = state;
-+ return (0, import_react18.createElement)(
++ return (0, import_react19.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-model-test" },
-+ (0, import_react18.createElement)(
++ (0, import_react19.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
-+ (0, import_react18.createElement)(
++ (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
++ (0, import_react19.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_react18.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
++ outcome?.code ? (0, import_react19.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ )
+ ),
-+ (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
++ (0, import_react19.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 = `
@@ -12322,11 +8742,11 @@ diff --git a/client.js b/client.js
+
// src/client/folder-menu.js
-var import_react14 = require("react");
-+var import_react19 = require("react");
++var import_react20 = 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_react19.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react19.createElement)(CheckIcon) : null);
++var Tick = ({ on }) => (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react20.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);
@@ -12334,165 +8754,165 @@ 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_react19.useState)(null);
-+ const [view, setView] = (0, import_react19.useState)("list");
-+ const [draft, setDraft] = (0, import_react19.useState)("");
-+ const [listing, setListing] = (0, import_react19.useState)(null);
-+ const [problem, setProblem] = (0, import_react19.useState)("");
++ const [info, setInfo] = (0, import_react20.useState)(null);
++ const [view, setView] = (0, import_react20.useState)("list");
++ const [draft, setDraft] = (0, import_react20.useState)("");
++ const [listing, setListing] = (0, import_react20.useState)(null);
++ const [problem, setProblem] = (0, import_react20.useState)("");
const fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
-@@ -6719,35 +6719,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
+@@ -6719,35 +10483,35 @@
};
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_react19.createElement)(
++ const listView = (0, import_react20.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react14.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-menu-label" }, fallback),
-+ (0, import_react19.createElement)(Tick, { on: !value })
++ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, fallback),
++ (0, import_react20.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_react19.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
-+ folders.map((folder) => (0, import_react19.createElement)(
++ folders.length > 0 ? (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
++ folders.map((folder) => (0, import_react20.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_react19.createElement)("span", { className: "bt-folder-copy" }, (0, import_react19.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react19.createElement)("span", { className: "bt-folder-path" }, folder.path)),
-+ (0, import_react19.createElement)(Tick, { on: folder.path === value })
++ (0, import_react20.createElement)("span", { className: "bt-folder-copy" }, (0, import_react20.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react20.createElement)("span", { className: "bt-folder-path" }, folder.path)),
++ (0, import_react20.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_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ info?.picker === "native" ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
-+ info?.picker === "browse" ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
-+ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
++ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ info?.picker === "native" ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
++ info?.picker === "browse" ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
++ (0, import_react20.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_react19.createElement)(PencilIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
++ } }, (0, import_react20.createElement)(PencilIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
);
- const typeView = (0, import_react14.createElement)(
-+ const typeView = (0, import_react19.createElement)(
++ const typeView = (0, import_react20.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_react19.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react19.createElement)(ChevronLeftIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react19.createElement)("input", {
++ (0, import_react20.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react20.createElement)(ChevronLeftIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
++ (0, import_react20.createElement)("input", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
-@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
+@@ -6759,33 +10523,33 @@
if (event.key === "Enter" && !event.nativeEvent.isComposing) typed();
}
}),
- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
-+ (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
++ (0, import_react20.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_react19.createElement)(
++ const browseView = listing ? (0, import_react20.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_react19.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react19.createElement)(ChevronLeftIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react19.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
-+ parent ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react19.createElement)(FolderIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
-+ inside.map((entry) => (0, import_react19.createElement)(
++ (0, import_react20.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react20.createElement)(ChevronLeftIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
++ (0, import_react20.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
++ parent ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react20.createElement)(FolderIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
++ inside.map((entry) => (0, import_react20.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_react19.createElement)(FolderIcon),
-+ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, entry.name),
-+ (0, import_react19.createElement)(ChevronRightIcon)
++ (0, import_react20.createElement)(FolderIcon),
++ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, entry.name),
++ (0, import_react20.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_react19.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
-+ listing.truncated ? (0, import_react19.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
-+ (0, import_react19.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react19.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder"))
++ inside.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
++ listing.truncated ? (0, import_react20.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
++ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react20.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_react19.createElement)(
-+ import_react19.Fragment,
++ return (0, import_react20.createElement)(
++ import_react20.Fragment,
null,
- (0, import_react14.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.createElement)(
"button",
{
ref: button,
-@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
+@@ -6798,19 +10562,19 @@
title: value,
onClick: toggle
},
- (0, import_react14.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
-+ value ? (0, import_react19.createElement)("span", { className: "bt-model-provider" }, value) : null
++ (0, import_react20.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
++ value ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, value) : null
),
- (0, import_react14.createElement)(Caret)
-+ (0, import_react19.createElement)(Caret)
++ (0, import_react20.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)(
-+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react19.createElement)(
++ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react20.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_react19.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
++ problem ? (0, import_react20.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
-@@ -6830,8 +6830,8 @@ var FOLDER_CSS = `
+@@ -6830,8 +10594,8 @@
`;
// src/client/schedules.js
-var import_react15 = require("react");
-var POLL_MS = 3e4;
-+var import_react20 = require("react");
++var import_react21 = require("react");
+var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
function publish(view) {
-@@ -6839,8 +6839,8 @@ function publish(view) {
+@@ -6839,8 +10603,8 @@
for (const listener of listeners) listener(view);
}
function useSchedules(actions) {
- const [view, setView] = (0, import_react15.useState)(shared);
- (0, import_react15.useEffect)(() => {
-+ const [view, setView] = (0, import_react20.useState)(shared);
-+ (0, import_react20.useEffect)(() => {
++ const [view, setView] = (0, import_react21.useState)(shared);
++ (0, import_react21.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
-@@ -6848,7 +6848,7 @@ function useSchedules(actions) {
+@@ -6848,7 +10612,7 @@
if (!stopped) publish(next);
}).catch(() => {
}).finally(() => {
@@ -12501,7 +8921,7 @@ diff --git a/client.js b/client.js
});
load();
return () => {
-@@ -6896,11 +6896,25 @@ function timingText(task) {
+@@ -6896,11 +10660,25 @@
return "";
}
}
@@ -12526,13 +8946,13 @@ diff --git a/client.js b/client.js
+ }
+}
+function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
-+ const [confirming, setConfirming] = (0, import_react20.useState)(false);
-+ const [busy, setBusy] = (0, import_react20.useState)(false);
-+ (0, import_react20.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react21.useState)(false);
++ const [busy, setBusy] = (0, import_react21.useState)(false);
++ (0, import_react21.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
-@@ -6913,20 +6913,48 @@ function TaskRow({ task, actions, onView, showBot, roster }) {
+@@ -6913,20 +10691,48 @@
setBusy(true);
void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false));
};
@@ -12544,7 +8964,7 @@ diff --git a/client.js b/client.js
+ onView(next);
+ }, () => setBusy(false));
+ };
-+ const [retryError, setRetryError] = (0, import_react20.useState)("");
++ const [retryError, setRetryError] = (0, import_react21.useState)("");
+ const retry = () => {
+ setBusy(true);
+ setRetryError("");
@@ -12561,45 +8981,45 @@ 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_react20.createElement)(
++ return (0, import_react21.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_react20.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
-+ (0, import_react20.createElement)(
++ showBot ? (0, import_react21.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-task-title" }, task.title),
-+ (0, import_react20.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_react20.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
-+ last ? (0, import_react20.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
-+ retryError ? (0, import_react20.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
++ (0, import_react21.createElement)("span", { className: "bt-task-title" }, task.title),
++ (0, import_react21.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_react21.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
++ last ? (0, import_react21.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
++ retryError ? (0, import_react21.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_react20.createElement)("span", { className: "bt-task-next" }, nextText(task)),
-+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react20.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_react20.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react20.createElement)(PencilIcon)) : null,
-+ stoppable ? (0, import_react20.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
-+ (0, import_react20.createElement)("button", {
++ (0, import_react21.createElement)("span", { className: "bt-task-next" }, nextText(task)),
++ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react21.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_react21.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react21.createElement)(PencilIcon)) : null,
++ stoppable ? (0, import_react21.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
++ (0, import_react21.createElement)("button", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
-@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) {
+@@ -6934,7 +10740,7 @@
"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_react15.createElement)(TrashIcon))
-+ }, confirming ? t("Delete") : (0, import_react20.createElement)(TrashIcon))
++ }, confirming ? t("Delete") : (0, import_react21.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
-@@ -6945,22 +6945,44 @@ function inAnHour() {
+@@ -6945,22 +10751,44 @@
}
var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]];
var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]];
@@ -12637,17 +9057,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_react20.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
-+ const [title, setTitle] = (0, import_react20.useState)(first.title);
-+ const [prompt, setPrompt] = (0, import_react20.useState)(first.prompt);
-+ const [kind, setKind] = (0, import_react20.useState)(first.kind);
-+ const [at, setAt] = (0, import_react20.useState)(first.at);
-+ const [time, setTime] = (0, import_react20.useState)(first.time);
-+ const [weekdays, setWeekdays] = (0, import_react20.useState)(first.weekdays);
-+ const [count, setCount] = (0, import_react20.useState)(first.count);
-+ const [unit, setUnit] = (0, import_react20.useState)(first.unit);
-+ const [busy, setBusy] = (0, import_react20.useState)(false);
-+ const [error, setError] = (0, import_react20.useState)("");
++ const [target, setTarget] = (0, import_react21.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
++ const [title, setTitle] = (0, import_react21.useState)(first.title);
++ const [prompt, setPrompt] = (0, import_react21.useState)(first.prompt);
++ const [kind, setKind] = (0, import_react21.useState)(first.kind);
++ const [at, setAt] = (0, import_react21.useState)(first.at);
++ const [time, setTime] = (0, import_react21.useState)(first.time);
++ const [weekdays, setWeekdays] = (0, import_react21.useState)(first.weekdays);
++ const [count, setCount] = (0, import_react21.useState)(first.count);
++ const [unit, setUnit] = (0, import_react21.useState)(first.unit);
++ const [busy, setBusy] = (0, import_react21.useState)(false);
++ const [error, setError] = (0, import_react21.useState)("");
const timing = () => {
+ if (kind === "cron") return { kind: "keep" };
if (kind === "at") return { kind, at: new Date(at).toISOString() };
@@ -12657,7 +9077,7 @@ diff --git a/client.js b/client.js
};
const submit = (event) => {
event.preventDefault();
-@@ -6975,146 +6975,185 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) {
+@@ -6975,146 +10803,185 @@
setBusy(false);
return;
}
@@ -12670,60 +9090,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_react20.createElement)(
++ return (0, import_react21.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react15.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
++ (0, import_react21.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_react20.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react20.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
-+ roster.rooms.length > 0 ? (0, import_react20.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react20.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
++ (0, import_react21.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react21.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
++ roster.rooms.length > 0 ? (0, import_react21.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react21.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
)
),
- (0, import_react15.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react20.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
++ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
++ (0, import_react21.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_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
-+ (0, import_react20.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
++ (0, import_react21.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_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("When"))),
-+ (0, import_react20.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react20.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("When"))),
++ (0, import_react21.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react21.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
-+ kind === "at" ? (0, import_react20.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_react20.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react20.createElement)("button", {
++ kind === "cron" ? (0, import_react21.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
++ kind === "at" ? (0, import_react21.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_react21.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react21.createElement)("button", {
key: day,
type: "button",
className: "bt-task-day",
@@ -12733,34 +9153,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_react20.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_react20.createElement)(
-+ import_react20.Fragment,
++ kind === "daily" || kind === "weekly" ? (0, import_react21.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_react21.createElement)(
++ import_react21.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_react20.createElement)("span", { className: "bt-task-every" }, t("Every")),
-+ (0, import_react20.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_react20.createElement)(
++ (0, import_react21.createElement)("span", { className: "bt-task-every" }, t("Every")),
++ (0, import_react21.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_react21.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_react20.createElement)("option", { key: id, value: id }, t(label)))
++ UNITS.map(([id, , label]) => (0, import_react21.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_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
-+ (0, import_react20.createElement)(
++ error ? (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
++ (0, import_react21.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_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ (0, import_react21.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")
@@ -12775,17 +9195,17 @@ diff --git a/client.js b/client.js
+ actions.selectPanel("plugins");
+}
+function AutomationOff({ actions }) {
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.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_react20.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react20.createElement)(ClockIcon)),
-+ (0, import_react20.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
-+ (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
-+ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ (0, import_react21.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react21.createElement)(ClockIcon)),
++ (0, import_react21.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
++ (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
++ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
);
}
function SchedulesPage({ roster, actions, start }) {
@@ -12793,10 +9213,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_react20.useState)(Boolean(start));
-+ const [editing, setEditing] = (0, import_react20.useState)(null);
-+ if (view === null) return (0, import_react20.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
-+ if (!view.available) return (0, import_react20.createElement)(AutomationOff, { actions });
++ const [adding, setAdding] = (0, import_react21.useState)(Boolean(start));
++ const [editing, setEditing] = (0, import_react21.useState)(null);
++ if (view === null) return (0, import_react21.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
++ if (!view.available) return (0, import_react21.createElement)(AutomationOff, { actions });
const shown = (next) => {
publish(next);
setAdding(false);
@@ -12809,53 +9229,53 @@ 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_react20.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_react20.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_react21.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_react21.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)(
-+ adding ? (0, import_react20.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react20.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
++ adding ? (0, import_react21.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react21.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
+ setEditing(null);
+ setAdding(true);
-+ } }, (0, import_react20.createElement)(PlusIcon), t("New task"))),
-+ byChat.length === 0 && !adding ? (0, import_react20.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react20.createElement)(
++ } }, (0, import_react21.createElement)(PlusIcon), t("New task"))),
++ byChat.length === 0 && !adding ? (0, import_react21.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react21.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_react20.createElement)("h3", { className: "bt-task-head" }, avatar, name),
-+ (0, import_react20.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
++ (0, import_react21.createElement)("h3", { className: "bt-task-head" }, avatar, name),
++ (0, import_react21.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_react20.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
-+ return (0, import_react20.createElement)(
-+ import_react20.Fragment,
++ const rows = tasks.filter((task) => task !== open).map((task) => (0, import_react21.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
++ return (0, import_react21.createElement)(
++ import_react21.Fragment,
+ null,
-+ open ? (0, import_react20.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
-+ rows.length + extra.length > 0 ? (0, import_react20.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
++ open ? (0, import_react21.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
++ rows.length + extra.length > 0 ? (0, import_react21.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
+ );
+}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
-+ const [editing, setEditing] = (0, import_react20.useState)(null);
++ const [editing, setEditing] = (0, import_react21.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_react20.createElement)(
++ return (0, import_react21.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")),
- (0, import_react15.createElement)(
-+ (0, import_react20.createElement)("h3", null, t("Scheduled tasks")),
-+ !view.available ? (0, import_react20.createElement)(
++ (0, import_react21.createElement)("h3", null, t("Scheduled tasks")),
++ !view.available ? (0, import_react21.createElement)(
"div",
{ className: "bt-set-card" },
- !view.available ? (0, import_react15.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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."))),
@@ -12865,10 +9285,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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
-+ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ (0, import_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
++ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ )
-+ ) : (0, import_react20.createElement)(TaskList, {
++ ) : (0, import_react21.createElement)(TaskList, {
+ tasks,
+ editing,
+ roster,
@@ -12879,7 +9299,7 @@ diff --git a/client.js b/client.js
+ setEditing(null);
+ },
+ onCancel: () => setEditing(null),
-+ extra: [(0, import_react20.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react20.createElement)(PlusIcon), t("New task"))]
++ extra: [(0, import_react21.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react21.createElement)(PlusIcon), t("New task"))]
+ })
);
}
@@ -12887,25 +9307,25 @@ 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_react20.createElement)(
++ return (0, import_react21.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_react20.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
-+ tasks.length === 0 ? (0, import_react20.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_react20.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react20.createElement)(
++ (0, import_react21.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
++ tasks.length === 0 ? (0, import_react21.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_react21.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react21.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_react20.createElement)(ClockIcon),
-+ (0, import_react20.createElement)("span", { className: "bt-task-line-title" }, task.title),
-+ (0, import_react20.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
++ (0, import_react21.createElement)(ClockIcon),
++ (0, import_react21.createElement)("span", { className: "bt-task-line-title" }, task.title),
++ (0, import_react21.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
)))
);
}
-@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = `
+@@ -7127,6 +10994,12 @@
.bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap}
.bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)}
.bt-task-remove svg{width:14px;height:14px}
@@ -12918,40 +9338,40 @@ diff --git a/client.js b/client.js
.bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135}
.bt-task-head{display:flex;align-items:center;gap:6px}
.bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out}
-@@ -7158,7 +7158,7 @@ var SCHEDULE_CSS = `
+@@ -7158,7 +11031,7 @@
`;
// src/client/capability-settings.js
-var import_react16 = require("react");
-+var import_react21 = require("react");
++var import_react22 = require("react");
var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] });
var parseMap = (text, label) => {
let value;
-@@ -7172,11 +7172,11 @@ var parseMap = (text, label) => {
+@@ -7172,11 +11045,11 @@
};
var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…");
function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) {
- return (0, import_react16.createElement)(
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t(label))),
-+ (0, import_react21.createElement)(multiline ? "textarea" : "input", {
++ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t(label))),
++ (0, import_react22.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
-@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl
+@@ -7188,7 +11061,7 @@
);
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react16.useState)("");
-+ const [error, setError] = (0, import_react21.useState)("");
++ const [error, setError] = (0, import_react22.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
-@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
+@@ -7199,7 +11072,7 @@
if (draft.kind === "mcp" && draft.transport === "stdio") {
payload.transport = "stdio";
payload.command = draft.command;
@@ -12960,94 +9380,94 @@ diff --git a/client.js b/client.js
payload.env = parseMap(draft.envText, t("Environment"));
} else if (draft.kind === "mcp") {
payload.transport = "streamable-http";
-@@ -7219,95 +7219,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
+@@ -7219,95 +11092,95 @@
}
};
const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind));
- return (0, import_react16.createElement)(
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.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_react21.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
++ (0, import_react22.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
++ (0, import_react22.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_react21.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
++ ["mcp", "skill", "pack"].map((kind) => (0, import_react22.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_react21.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
-+ draft.kind === "mcp" ? (0, import_react21.createElement)(
++ (0, import_react22.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
++ draft.kind === "mcp" ? (0, import_react22.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
++ (0, import_react22.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_react21.createElement)("option", { value: "stdio" }, t("Local command")),
-+ (0, import_react21.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
++ (0, import_react22.createElement)("option", { value: "stdio" }, t("Local command")),
++ (0, import_react22.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_react21.createElement)(
++ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react22.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_react21.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
-+ (0, import_react21.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
-+ (0, import_react21.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_react22.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
++ (0, import_react22.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
++ (0, import_react22.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_react21.createElement)(
++ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react22.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_react21.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
-+ (0, import_react21.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_react22.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
++ (0, import_react22.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_react21.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
-+ draft.kind === "pack" ? (0, import_react21.createElement)(
++ draft.kind === "skill" ? (0, import_react22.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
++ draft.kind === "pack" ? (0, import_react22.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_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react21.createElement)(
++ available.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react22.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_react21.createElement)("span", { className: "bt-set-copy" }, (0, import_react21.createElement)("span", { className: "bt-bs-label" }, item.name)),
-+ (0, import_react21.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_react22.createElement)("span", { className: "bt-set-copy" }, (0, import_react22.createElement)("span", { className: "bt-bs-label" }, item.name)),
++ (0, import_react22.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_react21.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
-+ (0, import_react21.createElement)(
++ error ? (0, import_react22.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
++ (0, import_react22.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_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
-+ (0, import_react21.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
++ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
++ (0, import_react22.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
)
);
}
@@ -13055,32 +9475,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_react21.createElement)(
++ return (0, import_react22.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react16.createElement)(
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react16.createElement)(
-+ (0, import_react21.createElement)(
++ (0, import_react22.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_react21.createElement)("span", { className: "bt-connector-name" }, item.name),
-+ (0, import_react21.createElement)("span", { className: "bt-connector-status" }, secondary)
++ (0, import_react22.createElement)("span", { className: "bt-connector-name" }, item.name),
++ (0, import_react22.createElement)("span", { className: "bt-connector-status" }, secondary)
),
- (0, import_react16.createElement)(
-+ (0, import_react21.createElement)(
++ (0, import_react22.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_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
-+ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
-+ (0, import_react21.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_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
++ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
++ (0, import_react22.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete"))
)
)
);
@@ -13090,59 +9510,59 @@ 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_react21.useState)(null);
-+ const [confirming, setConfirming] = (0, import_react21.useState)("");
-+ (0, import_react21.useEffect)(() => {
++ const [draft, setDraft] = (0, import_react22.useState)(null);
++ const [confirming, setConfirming] = (0, import_react22.useState)("");
++ (0, import_react22.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
-@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) {
+@@ -7330,20 +11203,20 @@
setConfirming("");
void run(actions.deleteCapability(item.name));
};
- return (0, import_react16.createElement)(
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react16.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react16.createElement)(
-+ (0, import_react21.createElement)("h3", null, t("Shared capabilities")),
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)("h3", null, t("Shared capabilities")),
++ (0, import_react22.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_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
++ (0, import_react22.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_react21.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
-+ (0, import_react21.createElement)(
++ draft ? (0, import_react22.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
++ (0, import_react22.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_react21.createElement)("div", { className: "bt-set-row" }, (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react21.createElement)(CapabilityRow, {
++ items.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-set-row" }, (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react22.createElement)(CapabilityRow, {
key: item.name,
item,
confirming: confirming === item.name,
-@@ -7359,10 +7359,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
+@@ -7359,10 +11232,10 @@
const attached = new Set(bot.capabilities ?? []);
const statuses = roster.capabilityStatuses?.[bot.id] ?? {};
if (items.length === 0) return null;
- return (0, import_react16.createElement)(
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react16.createElement)(SwitchRow2, {
-+ items.map((item) => (0, import_react21.createElement)(SwitchRow2, {
++ items.map((item) => (0, import_react22.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
-@@ -7372,9 +7372,55 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
+@@ -7372,9 +11245,55 @@
);
}
+// src/client/connector-bots.js
-+var import_react22 = require("react");
++var import_react23 = 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));
@@ -13158,31 +9578,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_react22.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_react23.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_react22.createElement)(
++ return (0, import_react23.createElement)(
+ "div",
+ { className: "bt-connector-bots" },
-+ bots.length === 0 ? (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react22.createElement)(
++ bots.length === 0 ? (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react23.createElement)(
+ "div",
+ { key: bot.id, className: "bt-connector-bot" },
-+ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react23.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ )),
-+ (0, import_react22.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_react23.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_react22.createElement)(
++ return (0, import_react23.createElement)(
+ Card2,
+ { title: t("Connectors") },
-+ connected.map((connector) => (0, import_react22.createElement)(
++ connected.map((connector) => (0, import_react23.createElement)(
+ "div",
+ { key: connector.id, className: "bt-connector-bot" },
-+ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react23.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ ))
+ );
+}
@@ -13190,19 +9610,19 @@ diff --git a/client.js b/client.js
// src/client/memory-panel.js
-var import_react17 = require("react");
-var POLL_MS2 = 1500;
-+var import_react23 = require("react");
++var import_react24 = require("react");
+var POLL_MS5 = 1500;
var MENU_EXIT_MS2 = 120;
var counts = /* @__PURE__ */ new Map();
var countListeners = /* @__PURE__ */ new Set();
-@@ -7384,13 +7384,13 @@ function noteCount(botId, count) {
+@@ -7384,13 +11303,13 @@
for (const listener of countListeners) listener();
}
function useMemoryCount(botId, actions) {
- const [count, setCount] = (0, import_react17.useState)(counts.get(botId) ?? null);
- (0, import_react17.useEffect)(() => {
-+ const [count, setCount] = (0, import_react23.useState)(counts.get(botId) ?? null);
-+ (0, import_react23.useEffect)(() => {
++ const [count, setCount] = (0, import_react24.useState)(counts.get(botId) ?? null);
++ (0, import_react24.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -13212,207 +9632,207 @@ diff --git a/client.js b/client.js
if (!stop) noteCount(botId, view.entries.length);
}).catch(() => {
});
-@@ -7404,16 +7404,16 @@ function useMemoryCount(botId, actions) {
+@@ -7404,16 +11323,16 @@
function MemoryRow({ bot, actions }) {
const count = useMemoryCount(bot.id, actions);
const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count });
- return (0, import_react17.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
"div",
null,
- (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react17.createElement)(
-+ (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Memory")),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Memory")),
++ (0, import_react24.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_react23.createElement)(MemoryIcon),
-+ (0, import_react23.createElement)("span", { className: "bt-mem-row-copy" }, label),
-+ (0, import_react23.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react23.createElement)(ChevronRightIcon))
++ (0, import_react24.createElement)(MemoryIcon),
++ (0, import_react24.createElement)("span", { className: "bt-mem-row-copy" }, label),
++ (0, import_react24.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react24.createElement)(ChevronRightIcon))
)
);
}
-@@ -7428,45 +7428,45 @@ function ago(at, now) {
+@@ -7428,45 +11347,45 @@
return format.format(-Math.round(hours / 24), "day");
}
function Skeleton({ label }) {
- return (0, import_react17.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react17.createElement)(
-+ [0, 1].map((block) => (0, import_react23.createElement)(
++ [0, 1].map((block) => (0, import_react24.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_react23.createElement)("span", { className: "bt-mem-skel-head" }),
-+ (0, import_react23.createElement)("span", null),
-+ (0, import_react23.createElement)("span", null),
-+ (0, import_react23.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
++ (0, import_react24.createElement)("span", { className: "bt-mem-skel-head" }),
++ (0, import_react24.createElement)("span", null),
++ (0, import_react24.createElement)("span", null),
++ (0, import_react24.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
)),
- label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
-+ label ? (0, import_react23.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
++ label ? (0, import_react24.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_react23.createElement)(
++ return (0, import_react24.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_react23.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react23.createElement)(MemoryIcon)),
-+ (0, import_react23.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
-+ (0, import_react23.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_react24.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react24.createElement)(MemoryIcon)),
++ (0, import_react24.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
++ (0, import_react24.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_react23.createElement)(
++ const failed = view.error && !view.summarizing ? (0, import_react24.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_react23.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_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
++ (0, import_react24.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_react24.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_react23.createElement)(import_react23.Fragment, null, failed, failed ? null : (0, import_react23.createElement)(Skeleton, { label: t("Writing the summary…") }));
-+ return (0, import_react23.createElement)(
-+ import_react23.Fragment,
++ if (!view.summary) return (0, import_react24.createElement)(import_react24.Fragment, null, failed, failed ? null : (0, import_react24.createElement)(Skeleton, { label: t("Writing the summary…") }));
++ return (0, import_react24.createElement)(
++ import_react24.Fragment,
null,
failed,
- (0, import_react17.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("h3", { key: index }, block.text) : (0, import_react23.createElement)("p", { key: index }, block.text))
++ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react24.createElement)("h3", { key: index }, block.text) : (0, import_react24.createElement)("p", { key: index }, block.text))
)
);
}
-@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => {
+@@ -7478,9 +11397,9 @@
return groups;
}, []);
function EntryRow({ entry, remove }) {
- 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_react23.useState)(false);
-+ const [busy, setBusy] = (0, import_react23.useState)(false);
-+ (0, import_react23.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react24.useState)(false);
++ const [busy, setBusy] = (0, import_react24.useState)(false);
++ (0, import_react24.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
-@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) {
+@@ -7495,16 +11414,16 @@
setBusy(true);
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react17.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react17.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
-+ meta ? (0, import_react23.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
++ (0, import_react24.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
++ meta ? (0, import_react24.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
),
- (0, import_react17.createElement)("button", {
-+ (0, import_react23.createElement)("button", {
++ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
-@@ -7512,7 +7512,7 @@ function EntryRow({ entry, remove }) {
+@@ -7512,7 +11431,7 @@
"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_react17.createElement)(TrashIcon))
-+ }, confirming ? t("Remove") : (0, import_react23.createElement)(TrashIcon))
++ }, confirming ? t("Remove") : (0, import_react24.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
-@@ -7521,25 +7521,25 @@ function Entries({ bot, view, remove }) {
+@@ -7521,25 +11440,25 @@
{ 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_react17.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react17.createElement)(
-+ sections.map((section) => (0, import_react23.createElement)(
++ sections.map((section) => (0, import_react24.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react17.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("h3", null, section.title),
-+ section.hint ? (0, import_react23.createElement)("span", null, section.hint) : null,
-+ (0, import_react23.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_react24.createElement)("h3", null, section.title),
++ section.hint ? (0, import_react24.createElement)("span", null, section.hint) : null,
++ (0, import_react24.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_react23.createElement)(
-+ import_react23.Fragment,
++ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react24.createElement)(
++ import_react24.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_react23.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
-+ (0, import_react23.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react23.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
-+ )) : (0, import_react23.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
++ group.topic ? (0, import_react24.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
++ (0, import_react24.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react24.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
++ )) : (0, import_react24.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
))
);
}
-@@ -7551,52 +7551,52 @@ function changeLine(change) {
+@@ -7551,52 +11470,52 @@
return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text });
}
function Reply({ answer, dismiss }) {
- return (0, import_react17.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react17.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
-+ answer.pending ? null : (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react23.createElement)(CloseIcon))
++ (0, import_react24.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
++ answer.pending ? null : (0, import_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react24.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_react23.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react23.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react23.createElement)(
-+ import_react23.Fragment,
++ answer.pending ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react24.createElement)(
++ import_react24.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_react23.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
-+ answer.changes.length > 0 ? (0, import_react23.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react23.createElement)(
++ answer.reply ? (0, import_react24.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
++ answer.changes.length > 0 ? (0, import_react24.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react24.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_react23.createElement)(CheckIcon) : (0, import_react23.createElement)(CloseIcon),
-+ (0, import_react23.createElement)("span", null, changeLine(change))
++ change.ok ? (0, import_react24.createElement)(CheckIcon) : (0, import_react24.createElement)(CloseIcon),
++ (0, import_react24.createElement)("span", null, changeLine(change))
))) : null
)
);
@@ -13424,18 +9844,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_react23.useState)(null);
-+ const [error, setError] = (0, import_react23.useState)("");
-+ const timer = (0, import_react23.useRef)(0);
-+ const latest = (0, import_react23.useRef)(0);
-+ const alive = (0, import_react23.useRef)(true);
-+ const show = (0, import_react23.useCallback)((next) => {
++ const [view, setView] = (0, import_react24.useState)(null);
++ const [error, setError] = (0, import_react24.useState)("");
++ const timer = (0, import_react24.useRef)(0);
++ const latest = (0, import_react24.useRef)(0);
++ const alive = (0, import_react24.useRef)(true);
++ const show = (0, import_react24.useCallback)((next) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react17.useCallback)(async (flags = {}) => {
-+ const load = (0, import_react23.useCallback)(async (flags = {}) => {
++ const load = (0, import_react24.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
@@ -13449,11 +9869,11 @@ diff --git a/client.js b/client.js
}
}, [botId]);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react23.useEffect)(() => {
++ (0, import_react24.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
-@@ -7606,9 +7606,9 @@ function useMemoryView(botId, actions) {
+@@ -7606,9 +11525,9 @@
}, [botId]);
return { view, error, load, show };
}
@@ -13461,19 +9881,19 @@ 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_react23.useState)(Date.now());
-+ (0, import_react23.useEffect)(() => {
++ const [now, setNow] = (0, import_react24.useState)(Date.now());
++ (0, import_react24.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
-@@ -7616,28 +7616,321 @@ function useNow(every) {
+@@ -7616,18 +11535,18 @@
}
function MemoryDialog({ bot, actions, leaving = false }) {
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_react23.useState)("summary");
-+ const [menu, setMenu] = (0, import_react23.useState)(false);
++ const [page, setPage] = (0, import_react24.useState)("summary");
++ const [menu, setMenu] = (0, import_react24.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);
@@ -13482,147 +9902,114 @@ 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_react23.useState)("");
-+ const [answer, setAnswer] = (0, import_react23.useState)(null);
-+ const menuRef = (0, import_react23.useRef)(null);
-+ const dotsRef = (0, import_react23.useRef)(null);
-+ const inputRef = (0, import_react23.useRef)(null);
++ const [text, setText] = (0, import_react24.useState)("");
++ const [answer, setAnswer] = (0, import_react24.useState)(null);
++ const menuRef = (0, import_react24.useRef)(null);
++ const dotsRef = (0, import_react24.useRef)(null);
++ const inputRef = (0, import_react24.useRef)(null);
+ const now = useNow3(3e4);
-+ const close = (0, import_react23.useCallback)(() => actions.closeMemory(), []);
++ const close = (0, import_react24.useCallback)(() => actions.closeMemory(), []);
useEscape(close);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react23.useEffect)(() => {
++ (0, import_react24.useEffect)(() => {
if (!menu) return void 0;
const onDown = (event) => {
-- if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
-- setMenu(false);
-+ if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
-+ setMenu(false);
-+ };
-+ const onKey = (event) => {
-+ if (event.key === "Escape") {
-+ event.stopPropagation();
-+ setMenu(false);
-+ dotsRef.current?.focus();
-+ }
-+ };
-+ window.addEventListener("mousedown", onDown);
-+ window.addEventListener("keydown", onKey, true);
-+ return () => {
-+ window.removeEventListener("mousedown", onDown);
-+ window.removeEventListener("keydown", onKey, true);
-+ };
-+ }, [menu]);
-+ const regenerate = () => {
-+ setMenu(false);
-+ setPage("summary");
-+ void load({ regenerate: true });
-+ };
-+ const openEntries = () => {
-+ setMenu(false);
-+ setPage("entries");
-+ };
-+ const backToSummary = () => {
-+ setPage("summary");
-+ void load({ summarize: true });
-+ };
-+ const remove = async (entry) => {
-+ try {
-+ show(await actions.memoryForget(bot.id, entry));
-+ return true;
-+ } catch (failure) {
-+ setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) });
-+ return false;
-+ }
-+ };
-+ const ask = async (event) => {
-+ event.preventDefault();
-+ const asked = text.trim();
-+ if (asked === "" || answer?.pending) return;
-+ setText("");
-+ setAnswer({ asked, pending: true });
-+ try {
-+ const result = await actions.memoryAsk(bot.id, asked);
-+ setAnswer({ asked, reply: result.reply, changes: result.changes });
-+ show(result.view);
-+ if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" });
-+ } catch (failure) {
-+ setAnswer({ asked, error: failure?.message ?? String(failure) });
-+ }
-+ inputRef.current?.focus();
-+ };
-+ const count = view?.entries.length ?? 0;
-+ 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_react23.createElement)(
-+ "div",
-+ { className: "bt-mem-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react23.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
-+ (0, import_react23.createElement)(
-+ "div",
-+ { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) },
-+ (0, import_react23.createElement)(
-+ "div",
-+ { className: "bt-mem-head" },
-+ page === "entries" ? (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react23.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react23.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
-+ (0, import_react23.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
-+ (0, import_react23.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react23.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_react23.createElement)(DotsIcon)),
-+ (0, import_react23.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react23.createElement)(CloseIcon)),
-+ menuShown ? (0, import_react23.createElement)(
-+ "div",
-+ { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 },
-+ (0, import_react23.createElement)(
-+ "button",
-+ { type: "button", role: "menuitem", onClick: openEntries },
-+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
-+ (0, import_react23.createElement)("span", { className: "bt-menu-hint" }, String(count))
-+ ),
-+ (0, import_react23.createElement)(
-+ "button",
-+ { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate },
-+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
-+ )
-+ ) : null
-+ ),
-+ (0, import_react23.createElement)(
-+ "div",
-+ { className: "bt-mem-body" },
-+ error ? (0, import_react23.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
-+ view === null ? error ? null : (0, import_react23.createElement)(Skeleton) : page === "entries" ? (0, import_react23.createElement)(Entries, { bot, view, remove }) : (0, import_react23.createElement)(Summary, { bot, view, regenerate })
-+ ),
-+ (0, import_react23.createElement)(
-+ "div",
-+ { className: "bt-mem-foot" },
-+ answer ? (0, import_react23.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
-+ (0, import_react23.createElement)(
-+ "form",
-+ { className: "bt-mem-ask", onSubmit: ask },
-+ (0, import_react23.createElement)("input", {
-+ ref: inputRef,
-+ className: "bt-mem-input",
-+ value: text,
-+ maxLength: 2e3,
-+ disabled: view === null,
-+ placeholder: t("Ask or update"),
-+ "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }),
-+ onChange: (event) => setText(event.target.value)
-+ }),
-+ (0, import_react23.createElement)(
-+ "button",
-+ { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") },
-+ busy ? (0, import_react23.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react23.createElement)(ArrowUpIcon)
-+ )
-+ )
-+ )
-+ )
-+ );
-+}
-+
+ if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
+@@ -7689,52 +11608,52 @@
+ 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_react17.createElement)(
++ return (0, import_react24.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_react24.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
++ (0, import_react24.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_react24.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,
+- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
+- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
+- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }),
+- (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_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react24.createElement)(ChevronLeftIcon)) : null,
++ (0, import_react24.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
++ (0, import_react24.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
++ (0, import_react24.createElement)("span", { className: "bt-mem-head-gap" }),
++ (0, import_react24.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_react24.createElement)(DotsIcon)),
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react24.createElement)(CloseIcon)),
++ menuShown ? (0, import_react24.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_react24.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_react24.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
++ (0, import_react24.createElement)("span", { className: "bt-menu-hint" }, String(count))
+ ),
+- (0, import_react17.createElement)(
++ (0, import_react24.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_react24.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
+ )
+ ) : null
+ ),
+- (0, import_react17.createElement)(
++ (0, import_react24.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_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
++ view === null ? error ? null : (0, import_react24.createElement)(Skeleton) : page === "entries" ? (0, import_react24.createElement)(Entries, { bot, view, remove }) : (0, import_react24.createElement)(Summary, { bot, view, regenerate })
+ ),
+- (0, import_react17.createElement)(
++ (0, import_react24.createElement)(
+ "div",
+ { className: "bt-mem-foot" },
+- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
+- (0, import_react17.createElement)(
++ answer ? (0, import_react24.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
++ (0, import_react24.createElement)(
+ "form",
+ { className: "bt-mem-ask", onSubmit: ask },
+- (0, import_react17.createElement)("input", {
++ (0, import_react24.createElement)("input", {
+ ref: inputRef,
+ className: "bt-mem-input",
+ value: text,
+@@ -7744,10 +11663,10 @@
+ "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }),
+ onChange: (event) => setText(event.target.value)
+ }),
+- (0, import_react17.createElement)(
++ (0, import_react24.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_react24.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react24.createElement)(ArrowUpIcon)
+ )
+ )
+ )
+@@ -7755,8 +11674,40 @@
+ );
+ }
+
+// src/client/standing-approvals.js
-+var import_react24 = require("react");
++var import_react25 = require("react");
+var standingOf = (target, roster) => {
+ const me = roster?.self?.memberId;
+ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -13631,19 +10018,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_react24.createElement)(
++ return (0, import_react25.createElement)(
+ Card2,
+ { title: t("Always allowed") },
-+ grants.map((grant) => (0, import_react24.createElement)(
++ grants.map((grant) => (0, import_react25.createElement)(
+ "div",
+ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react24.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
-+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, hint)
++ (0, import_react25.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
++ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint)
+ ),
-+ (0, import_react24.createElement)("button", {
++ (0, import_react25.createElement)("button", {
+ type: "button",
+ className: "bt-soft bt-soft-sm",
+ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -13653,458 +10040,424 @@ diff --git a/client.js b/client.js
+ );
+}
+
-+// src/client/bot-settings.js
-+var import_react25 = 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"));
-+ if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB"));
-+ const bitmap = await createImageBitmap(file);
-+ const side = 160;
-+ const canvas = document.createElement("canvas");
-+ canvas.width = side;
-+ canvas.height = side;
-+ const context = canvas.getContext("2d");
-+ context.imageSmoothingQuality = "high";
-+ const crop = Math.min(bitmap.width, bitmap.height);
-+ context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side);
-+ bitmap.close?.();
-+ for (const quality of [0.9, 0.75, 0.6]) {
-+ const url = canvas.toDataURL("image/webp", quality);
-+ if (url.startsWith("data:image/webp") && url.length < 15e4) return url;
-+ }
-+ const png = canvas.toDataURL("image/png");
-+ if (png.length < 15e4) return png;
-+ 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_react25.useState)(null);
-+ const reverting = (0, import_react25.useRef)(false);
-+ const commit = () => {
-+ const next = draft?.trim();
-+ setDraft(null);
-+ if (reverting.current) {
-+ reverting.current = false;
-+ return;
-+ }
-+ if (next === void 0 || next === (value ?? "") || required && next === "") return;
-+ void onSave(next);
-+ };
-+ return (0, import_react25.createElement)(multiline ? "textarea" : "input", {
-+ className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
-+ value: draft ?? value ?? "",
-+ placeholder,
-+ maxLength,
-+ required,
-+ "aria-label": label,
-+ spellCheck: false,
-+ onChange: (event) => setDraft(event.target.value),
-+ onBlur: commit,
-+ onKeyDown: (event) => {
-+ if (event.key === "Escape") {
-+ event.stopPropagation();
-+ reverting.current = true;
-+ event.currentTarget.blur();
-+ }
-+ if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur();
-+ }
-+ });
-+}
-+function Row({ label, hint, stack = false, children }) {
-+ return (0, import_react25.createElement)(
-+ "label",
-+ { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ children
-+ );
-+}
-+function Card({ title, children }) {
-+ return (0, import_react25.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react25.createElement)("h3", null, title) : null, (0, import_react25.createElement)("div", { className: "bt-set-card" }, children));
-+}
-+function Switch({ on, label, disabled, onChange }) {
-+ return (0, import_react25.createElement)(
-+ "button",
-+ { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) },
-+ (0, import_react25.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
-+ );
-+}
+ // src/client/bot-settings.js
+-var import_react18 = require("react");
++var import_react26 = 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"));
+@@ -7780,8 +11731,8 @@
+ 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_react18.useState)(null);
+- const reverting = (0, import_react18.useRef)(false);
++ const [draft, setDraft] = (0, import_react26.useState)(null);
++ const reverting = (0, import_react26.useRef)(false);
+ const commit = () => {
+ const next = draft?.trim();
+ setDraft(null);
+@@ -7792,7 +11743,7 @@
+ if (next === void 0 || next === (value ?? "") || required && next === "") return;
+ void onSave(next);
+ };
+- return (0, import_react18.createElement)(multiline ? "textarea" : "input", {
++ return (0, import_react26.createElement)(multiline ? "textarea" : "input", {
+ className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
+ value: draft ?? value ?? "",
+ placeholder,
+@@ -7813,29 +11764,29 @@
+ });
+ }
+ function Row({ label, hint, stack = false, children }) {
+- return (0, import_react18.createElement)(
++ return (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react26.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_react26.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react26.createElement)("h3", null, title) : null, (0, import_react26.createElement)("div", { className: "bt-set-card" }, children));
+ }
+ function Switch({ on, label, disabled, onChange }) {
+- return (0, import_react18.createElement)(
++ return (0, import_react26.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_react26.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_react25.createElement)(
-+ "div",
-+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ (0, import_react25.createElement)(Switch, { on, label, disabled, onChange })
-+ );
-+}
-+function modelHint(bot, roster) {
-+ const shown = modelOf(bot, roster);
-+ if (shown === void 0) return t("Pick the model this Bot runs on.");
-+ if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one.");
-+ if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it.");
-+ return t("Applies from the next message.");
-+}
-+function ShapeRing({ shape, size }) {
-+ const maskId = (0, import_react25.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_react25.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_react25.createElement)(
-+ import_react25.Fragment,
-+ null,
-+ (0, import_react25.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react25.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react25.createElement)("button", {
-+ key: id,
-+ type: "button",
-+ className: "bt-shape",
-+ "aria-label": t(shapeOf(id).label),
-+ title: t(shapeOf(id).label),
-+ "aria-pressed": !hasPhoto && look.shape === id,
-+ onClick: () => onShape(id)
-+ }, (0, import_react25.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react25.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
-+ (0, import_react25.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react25.createElement)("button", {
-+ key: color,
-+ type: "button",
-+ className: "bt-color",
-+ "aria-label": t(capitalize(color)),
-+ title: t(capitalize(color)),
-+ "aria-pressed": look.color === color,
-+ onClick: () => onColor(color)
-+ }, (0, import_react25.createElement)("span", { style: { background: paintCss(color) } }))))
-+ );
-+}
-+function AvatarEditor({ bot, actions, run, onClose }) {
-+ const look = lookOf(bot);
-+ const [tab, setTab] = (0, import_react25.useState)(look.image ? "upload" : "bot");
-+ const [over, setOver] = (0, import_react25.useState)(false);
-+ const ref = (0, import_react25.useRef)(null);
-+ const fileRef = (0, import_react25.useRef)(null);
-+ const upload = (file) => {
-+ if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } })));
-+ };
-+ (0, import_react25.useEffect)(() => {
-+ const onDown = (event) => {
-+ if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
-+ onClose();
- };
- const onKey = (event) => {
- if (event.key === "Escape") {
- event.stopPropagation();
-- setMenu(false);
-- dotsRef.current?.focus();
-+ onClose();
- }
- };
- window.addEventListener("mousedown", onDown);
-@@ -7646,514 +7646,896 @@ function MemoryDialog({ bot, actions, leaving = false }) {
- window.removeEventListener("mousedown", onDown);
++ return (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) : null),
++ (0, import_react26.createElement)(Switch, { on, label, disabled, onChange })
+ );
+ }
+ function modelHint(bot, roster) {
+@@ -7846,22 +11797,22 @@
+ return t("Applies from the next message.");
+ }
+ function ShapeRing({ shape, size }) {
+- const maskId = (0, import_react18.useRef)("");
++ const maskId = (0, import_react26.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_react26.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_react18.createElement)(
+- import_react18.Fragment,
++ return (0, import_react26.createElement)(
++ import_react26.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_react26.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react26.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react26.createElement)("button", {
+ key: id,
+ type: "button",
+ className: "bt-shape",
+@@ -7869,8 +11820,8 @@
+ title: t(shapeOf(id).label),
+ "aria-pressed": !hasPhoto && look.shape === id,
+ 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_react26.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react26.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
++ (0, import_react26.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react26.createElement)("button", {
+ key: color,
+ type: "button",
+ className: "bt-color",
+@@ -7878,19 +11829,19 @@
+ title: t(capitalize(color)),
+ "aria-pressed": look.color === color,
+ onClick: () => onColor(color)
+- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } }))))
++ }, (0, import_react26.createElement)("span", { style: { background: paintCss(color) } }))))
+ );
+ }
+ function AvatarEditor({ bot, actions, run, onClose }) {
+ const look = lookOf(bot);
+- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot");
+- 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_react26.useState)(look.image ? "upload" : "bot");
++ const [over, setOver] = (0, import_react26.useState)(false);
++ const ref = (0, import_react26.useRef)(null);
++ const fileRef = (0, import_react26.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_react26.useEffect)(() => {
+ const onDown = (event) => {
+ if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
+ onClose();
+@@ -7908,7 +11859,7 @@
window.removeEventListener("keydown", onKey, true);
};
-- }, [menu]);
-- const regenerate = () => {
-- setMenu(false);
-- setPage("summary");
-- void load({ regenerate: true });
-- };
-- const openEntries = () => {
-- setMenu(false);
-- setPage("entries");
-- };
-- const backToSummary = () => {
-- setPage("summary");
-- void load({ summarize: true });
-- };
-- const remove = async (entry) => {
-- try {
-- show(await actions.memoryForget(bot.id, entry));
-- return true;
-- } catch (failure) {
-- setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) });
-- return false;
-- }
-- };
-- const ask = async (event) => {
-- event.preventDefault();
-- const asked = text.trim();
-- if (asked === "" || answer?.pending) return;
-- setText("");
-- setAnswer({ asked, pending: true });
-- try {
-- const result = await actions.memoryAsk(bot.id, asked);
-- setAnswer({ asked, reply: result.reply, changes: result.changes });
-- show(result.view);
-- if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" });
-- } catch (failure) {
-- setAnswer({ asked, error: failure?.message ?? String(failure) });
-- }
-- inputRef.current?.focus();
-+ }, [onClose]);
-+ (0, import_react25.useEffect)(() => {
-+ if (tab !== "upload") return void 0;
-+ const onPaste = (event) => {
-+ const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
-+ if (file) {
-+ event.preventDefault();
-+ upload(file);
-+ }
-+ };
-+ window.addEventListener("paste", onPaste);
-+ return () => window.removeEventListener("paste", onPaste);
-+ }, [tab, bot]);
-+ const hasPhoto = Boolean(look.image);
-+ const glide = useGlide(tab);
-+ return (0, import_react25.createElement)(
-+ "div",
-+ { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-avedit-top" },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" },
-+ (0, import_react25.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react25.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
-+ ),
-+ (0, import_react25.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_react25.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_react25.createElement)(
-+ "div",
-+ {
-+ className: "bt-dropzone",
-+ "data-over": over || void 0,
-+ onDragOver: (event) => {
-+ event.preventDefault();
-+ setOver(true);
-+ },
-+ onDragLeave: () => setOver(false),
-+ onDrop: (event) => {
-+ event.preventDefault();
-+ setOver(false);
-+ upload(event.dataTransfer.files?.[0]);
-+ }
-+ },
-+ hasPhoto ? (0, import_react25.createElement)("img", { src: look.image, alt: "" }) : null,
-+ (0, import_react25.createElement)("div", null, t("Drag, drop, or paste an image")),
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-actions", style: { justifyContent: "center" } },
-+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
-+ hasPhoto ? (0, import_react25.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_react25.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);
-+ } })
-+ ) : null
-+ );
-+}
-+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_react25.createElement)(
-+ Card,
-+ { title: t("Memory") },
-+ (0, import_react25.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, label)),
-+ (0, import_react25.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react25.createElement)(ChevronRightIcon))
-+ )
-+ );
-+}
-+function BotSettingsForm({ bot, roster, actions, run }) {
-+ const [editing, setEditing] = (0, import_react25.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react25.useState)(false);
-+ const closeEditor = (0, import_react25.useCallback)(() => setEditing(false), []);
-+ (0, import_react25.useEffect)(() => {
-+ if (!confirming) return void 0;
-+ const timer = setTimeout(() => setConfirming(false), 4e3);
-+ return () => clearTimeout(timer);
-+ }, [confirming]);
+ }, [onClose]);
+- (0, import_react18.useEffect)(() => {
++ (0, import_react26.useEffect)(() => {
+ if (tab !== "upload") return void 0;
+ const onPaste = (event) => {
+ const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
+@@ -7922,32 +11873,32 @@
+ }, [tab, bot]);
+ const hasPhoto = Boolean(look.image);
+ const glide = useGlide(tab);
+- return (0, import_react18.createElement)(
++ return (0, import_react26.createElement)(
+ "div",
+ { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-avedit-top" },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react26.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_react26.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_react18.createElement)(CharacterGrid, {
++ tab === "bot" ? (0, import_react26.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_react26.createElement)(
+ "div",
+ {
+ className: "bt-dropzone",
+@@ -7963,15 +11914,15 @@
+ upload(event.dataTransfer.files?.[0]);
+ }
+ },
+- 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_react26.createElement)("img", { src: look.image, alt: "" }) : null,
++ (0, import_react26.createElement)("div", null, t("Drag, drop, or paste an image")),
++ (0, import_react26.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_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
++ hasPhoto ? (0, import_react26.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_react26.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 +11933,27 @@
+ function MemoryCard({ bot, actions }) {
+ const count = useMemoryCount(bot.id, actions);
+ const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count });
+- return (0, import_react18.createElement)(
++ return (0, import_react26.createElement)(
+ Card,
+ { title: t("Memory") },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, label)),
++ (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon))
+ )
+ );
+ }
+ function BotSettingsForm({ bot, roster, actions, run }) {
+- const [editing, setEditing] = (0, import_react18.useState)(false);
+- 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_react26.useState)(false);
++ const [confirming, setConfirming] = (0, import_react26.useState)(false);
++ const closeEditor = (0, import_react26.useCallback)(() => setEditing(false), []);
++ (0, import_react26.useEffect)(() => {
+ if (!confirming) return void 0;
+ const timer = setTimeout(() => setConfirming(false), 4e3);
+ return () => clearTimeout(timer);
+ }, [confirming]);
+ if (bot.remote) return null;
-+ const isMainBot = isMainOf(roster, bot.id);
-+ const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1;
-+ const isTile = roster.mainBotId === bot.id;
-+ 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);
+ const isMainBot = isMainOf(roster, bot.id);
+ const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1;
+ const isTile = roster.mainBotId === bot.id;
+@@ -8011,112 +11963,116 @@
+ if (confirming) void run(actions.deleteBot(bot.id));
+ else setConfirming(true);
};
-- const count = view?.entries.length ?? 0;
-- 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_react17.createElement)(
-+ (0, import_react25.useEffect)(() => {
-+ void actions.refreshModels?.();
-+ }, [bot.id]);
-+ return (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-bs" },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-bs-hero" },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-avatar-anchor" },
-+ (0, import_react25.createElement)(
-+ "button",
-+ { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) },
-+ (0, import_react25.createElement)(BotMark, { bot, size: 64, live: true }),
-+ (0, import_react25.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react25.createElement)(PencilIcon))
-+ ),
-+ editing ? (0, import_react25.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
-+ ),
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-bs-who" },
-+ (0, import_react25.createElement)("span", { className: "bt-bs-name" }, bot.name),
-+ (0, import_react25.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
-+ )
-+ ),
-+ (0, import_react25.createElement)(
-+ Card,
-+ { title: t("Profile") },
-+ (0, import_react25.createElement)(Row, { label: t("Name") }, (0, import_react25.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
-+ (0, import_react25.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react25.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
-+ ),
-+ (0, import_react25.createElement)(
-+ Card,
-+ { title: t("Model") },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
-+ (0, import_react25.createElement)(ModelMenu, { bot, roster, actions, run })
+- (0, import_react18.useEffect)(() => {
++ (0, import_react26.useEffect)(() => {
+ void actions.refreshModels?.();
+ }, [bot.id]);
+- return (0, import_react18.createElement)(
++ return (0, import_react26.createElement)(
+ "div",
+ { className: "bt-bs" },
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-bs-hero" },
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-avatar-anchor" },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)(BotMark, { bot, size: 64, live: true }),
++ (0, import_react26.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react26.createElement)(PencilIcon))
+ ),
+- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
++ editing ? (0, import_react26.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
+ ),
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-bs-name" }, bot.name),
++ (0, import_react26.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
+ )
+ ),
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)(Row, { label: t("Name") }, (0, import_react26.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
++ (0, import_react26.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react26.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
+ ),
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Model") },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
++ (0, import_react26.createElement)(ModelMenu, { bot, roster, actions, run })
+ ),
-+ bot.model ? (0, import_react25.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
-+ ),
-+ (0, import_react25.createElement)(
-+ Card,
-+ { title: t("Working folder") },
-+ (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
-+ (0, import_react25.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
-+ )
-+ ),
-+ (0, import_react25.createElement)(
-+ Card,
-+ { title: t("Instructions") },
-+ (0, import_react25.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_react25.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
-+ )
-+ ),
++ bot.model ? (0, import_react26.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
+ ),
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Working folder") },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
++ (0, import_react26.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
+ )
+ ),
+- (0, import_react18.createElement)(
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Instructions") },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
+ )
+ ),
+- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
+- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }),
+- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }),
+- (0, import_react18.createElement)(
+ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406).
-+ (0, import_react25.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react25.createElement)(Card, { title: t("Sharing") }, children) }),
-+ (0, import_react25.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ (0, import_react25.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ roster.memory === false ? null : (0, import_react25.createElement)(MemoryCard, { bot, actions }),
-+ (0, import_react25.createElement)(BotSchedules, { bot, roster, actions }),
-+ (0, import_react25.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
-+ (0, import_react25.createElement)(
-+ Card,
-+ { title: t("Main Bot") },
-+ onlyMain ? (0, import_react25.createElement)(
-+ "div",
-+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react25.createElement)(
-+ "span",
-+ { className: "bt-set-copy" },
-+ (0, import_react25.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_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
-+ ) : (0, import_react25.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_react25.createElement)(
-+ Card,
-+ { title: t("Sidebar") },
-+ (0, import_react25.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_react25.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_react25.createElement)(
-+ Card,
++ (0, import_react26.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react26.createElement)(Card, { title: t("Sharing") }, children) }),
++ (0, import_react26.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
++ (0, import_react26.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
++ roster.memory === false ? null : (0, import_react26.createElement)(MemoryCard, { bot, actions }),
++ (0, import_react26.createElement)(BotSchedules, { bot, roster, actions }),
++ (0, import_react26.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Main Bot") },
+- onlyMain ? (0, import_react18.createElement)(
++ onlyMain ? (0, import_react26.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row" },
+- (0, import_react18.createElement)(
++ (0, import_react26.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_react26.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_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
++ ) : (0, import_react26.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_react26.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_react26.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_react26.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_react26.createElement)(
+ Card,
+- { title: t("Manage") },
+- (0, import_react18.createElement)(
+ { title: t("More") },
-+ (0, import_react25.createElement)(
-+ "button",
-+ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
-+ (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
-+ ),
-+ isMainBot ? null : (0, import_react25.createElement)(
-+ "button",
-+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
-+ (0, import_react25.createElement)(
-+ "span",
-+ { className: "bt-set-copy" },
-+ (0, import_react25.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
-+ (0, import_react25.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_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.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%}
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
++ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
+ ),
+- isMainBot ? null : (0, import_react18.createElement)(
++ isMainBot ? null : (0, import_react26.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_react26.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_react26.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
++ (0, import_react26.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_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
+ )
+ )
+ );
+@@ -8133,6 +12089,7 @@
+ .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}
-+.bt-bs-input::placeholder{color:var(--bt-ink-3)}
-+.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px}
-+.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)}
-+.bt-bs-area:hover{background:var(--bt-active)}
-+.bt-bs-danger .bt-bs-label{color:#c21d2e}
-+.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)}
-+.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)}
-+.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease}
-+.bt-switch[aria-checked=true]{background:var(--bt-accent)}
-+.bt-switch:disabled{opacity:.45;cursor:default}
-+.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px}
-+.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}}
-+.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)}
-+.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px}
-+.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)}
-+.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out}
-+@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}}
-+`;
-+
+ .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease}
+ .bt-bs-input:hover{background:var(--bt-hover)}
+ .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left}
+@@ -8155,6 +12112,643 @@
+ @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_react26 = require("react");
++var import_react27 = 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."]
@@ -14136,9 +10489,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_react26.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
++ marks.push((0, import_react27.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
+ }
-+ if (marks.length === 0) return [(0, import_react26.createElement)(PersonMark, { key: "person", size, name: person?.name })];
++ if (marks.length === 0) return [(0, import_react27.createElement)(PersonMark, { key: "person", size, name: person?.name })];
+ return marks;
+}
+function faceRoomCount(room2, roster) {
@@ -14152,124 +10505,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_react26.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_react26.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_react26.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
-+ return index === 0 ? marked : [(0, import_react26.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
++ const name = seat.local && onOpenBot ? (0, import_react27.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_react27.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_react27.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
++ return index === 0 ? marked : [(0, import_react27.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
+ });
-+ 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)(
-- "div",
-- { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) },
-- (0, import_react17.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,
-- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
-- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
-- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }),
-- (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)(
-- "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)(
-- "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_react17.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"))
-- )
-- ) : null
-- ),
-- (0, import_react17.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 })
-- ),
-- (0, import_react17.createElement)(
-- "div",
-- { className: "bt-mem-foot" },
-- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
-- (0, import_react17.createElement)(
-- "form",
-- { className: "bt-mem-ask", onSubmit: ask },
-- (0, import_react17.createElement)("input", {
-- ref: inputRef,
-- className: "bt-mem-input",
-- value: text,
-- maxLength: 2e3,
-- disabled: view === null,
-- placeholder: t("Ask or update"),
-- "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }),
-- onChange: (event) => setText(event.target.value)
-- }),
-- (0, import_react17.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)
-- )
-- )
++ return (0, import_react27.createElement)(
++ "div",
+ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-gs-who bt-face-person" },
-+ (0, import_react26.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-face-person-text" },
-+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
-+ (0, import_react26.createElement)("span", { className: "bt-face-bots" }, bots)
- )
- )
- );
- }
--
--// src/client/bot-settings.js
--var import_react18 = 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"));
-- if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB"));
-- const bitmap = await createImageBitmap(file);
-- const side = 160;
-- const canvas = document.createElement("canvas");
-- canvas.width = side;
-- canvas.height = side;
-- const context = canvas.getContext("2d");
-- context.imageSmoothingQuality = "high";
-- const crop = Math.min(bitmap.width, bitmap.height);
-- context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side);
-- bitmap.close?.();
-- for (const quality of [0.9, 0.75, 0.6]) {
-- const url = canvas.toDataURL("image/webp", quality);
-- if (url.startsWith("data:image/webp") && url.length < 15e4) return url;
-- }
-- const png = canvas.toDataURL("image/png");
-- if (png.length < 15e4) return png;
-- 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_react18.useState)(null);
-- const reverting = (0, import_react18.useRef)(false);
-- const commit = () => {
-- const next = draft?.trim();
-- setDraft(null);
-- if (reverting.current) {
-- reverting.current = false;
-- return;
++ (0, import_react27.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
++ (0, import_react27.createElement)("span", { className: "bt-face-bots" }, bots)
++ )
++ )
++ );
++}
+function FaceLead({ room: room2, roster, pick: pick2 }) {
+ const lead = room2.face?.lead;
-+ const [open, setOpen] = (0, import_react26.useState)(false);
-+ const [busy, setBusy] = (0, import_react26.useState)(false);
-+ const [error, setError] = (0, import_react26.useState)("");
++ const [open, setOpen] = (0, import_react27.useState)(false);
++ const [busy, setBusy] = (0, import_react27.useState)(false);
++ const [error, setError] = (0, import_react27.useState)("");
+ const seats = faceSeats(room2, roster);
+ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
+ if (!current) return null;
@@ -14284,134 +10544,50 @@ diff --git a/client.js b/client.js
+ setError(err?.message || t("Could not change the room lead."));
+ } finally {
+ setBusy(false);
- }
-- if (next === void 0 || next === (value ?? "") || required && next === "") return;
-- void onSave(next);
- };
-- return (0, import_react18.createElement)(multiline ? "textarea" : "input", {
-- className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
-- value: draft ?? value ?? "",
-- placeholder,
-- maxLength,
-- required,
-- "aria-label": label,
-- spellCheck: false,
-- onChange: (event) => setDraft(event.target.value),
-- onBlur: commit,
-- onKeyDown: (event) => {
-- if (event.key === "Escape") {
-- event.stopPropagation();
-- reverting.current = true;
-- event.currentTarget.blur();
-- }
-- if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur();
-- }
-- });
--}
--function Row({ label, hint, stack = false, children }) {
-- return (0, import_react18.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),
-- 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));
--}
--function Switch({ on, label, disabled, onChange }) {
-- return (0, import_react18.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 })
-- );
--}
--function SwitchRow({ label, hint, on, disabled, onChange }) {
-- return (0, import_react18.createElement)(
-+ return (0, import_react26.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 })
++ }
++ };
++ return (0, import_react27.createElement)(
++ "div",
+ { className: "bt-face-lead", "data-flybot": "face-lead" },
-+ (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-set-row" },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
-+ (0, import_react26.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_react27.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
++ (0, import_react27.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_react26.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
++ canPick ? (0, import_react27.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_react26.createElement)(
++ open && canPick ? seats.map((seat) => (0, import_react27.createElement)(
+ "div",
+ { key: seat.key, className: "bt-set-row" },
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
-+ seat === current ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
++ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
++ seat === current ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react27.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_react26.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_react26.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
-+ error ? (0, import_react26.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
- );
- }
--function modelHint(bot, roster) {
-- const shown = modelOf(bot, roster);
-- if (shown === void 0) return t("Pick the model this Bot runs on.");
-- if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one.");
-- if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it.");
-- return t("Applies from the next message.");
++ open && canPick && lead.picked ? (0, import_react27.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_react27.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
++ error ? (0, import_react27.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
++ );
++}
+function seatedCount(room2) {
+ const live = room2?.presentedParticipants ?? room2?.participants ?? [];
+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length;
+ const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length;
+ return humans + remotes + (room2?.members?.length ?? 0);
- }
--function ShapeRing({ shape, size }) {
-- const maskId = (0, import_react18.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 } });
++}
+function roomSubtitle(room2, roster) {
+ if (room2.origin === "face-to-face") return faceRoomCount(room2, roster);
+ const admin = roster.byId[room2.admin];
+ const count = seatedCount(room2);
+ return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · ");
- }
--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_react18.createElement)(
-- import_react18.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", {
-- key: id,
-- type: "button",
-- className: "bt-shape",
-- "aria-label": t(shapeOf(id).label),
-- title: t(shapeOf(id).label),
-- "aria-pressed": !hasPhoto && look.shape === id,
-- 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", {
-- key: color,
-- type: "button",
-- className: "bt-color",
-- "aria-label": t(capitalize(color)),
-- title: t(capitalize(color)),
-- "aria-pressed": look.color === color,
-- onClick: () => onColor(color)
-- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } }))))
++}
+function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
-+ const [adding, setAdding] = (0, import_react26.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react26.useState)(false);
-+ (0, import_react26.useEffect)(() => {
++ const [adding, setAdding] = (0, import_react27.useState)(false);
++ const [confirming, setConfirming] = (0, import_react27.useState)(false);
++ (0, import_react27.useEffect)(() => {
+ if (!confirming) return void 0;
+ const timer = setTimeout(() => setConfirming(false), 4e3);
+ return () => clearTimeout(timer);
@@ -14449,41 +10625,41 @@ diff --git a/client.js b/client.js
+ if (done) onDeleted?.();
+ });
+ };
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
+ "div",
+ { className: "bt-bs" },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-bs-hero" },
-+ (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-bs-who" },
-+ (0, import_react26.createElement)("span", { className: "bt-bs-name" }, room2.name),
-+ (0, import_react26.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
++ (0, import_react27.createElement)("span", { className: "bt-bs-name" }, room2.name),
++ (0, import_react27.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ )
+ ),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ Card,
+ { title: t("Profile") },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ Row,
+ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
-+ (0, import_react26.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
++ (0, import_react27.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
+ ),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ Row,
+ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
-+ (0, import_react26.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_react27.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_react26.createElement)(
++ (0, import_react27.createElement)(
+ Card,
+ { title: t("Who answers") },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react26.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react26.createElement)("button", {
++ (0, import_react27.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react27.createElement)("button", {
+ key: id,
+ type: "button",
+ "aria-pressed": mode === id,
@@ -14493,104 +10669,104 @@ diff --git a/client.js b/client.js
+ if (mode !== id) void update({ mode: id });
+ }
+ }, t(label)))),
-+ (0, import_react26.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
++ (0, import_react27.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ )
+ ),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ Card,
+ { title: t("Working folder") },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
-+ (0, import_react26.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ 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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
+ )
+ ),
-+ face ? (0, import_react26.createElement)(
++ face ? (0, import_react27.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: people2.length }) },
-+ people2.map((person) => (0, import_react26.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
++ people2.map((person) => (0, import_react27.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ ) : null,
-+ face ? null : (0, import_react26.createElement)(
++ face ? null : (0, import_react27.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
-+ humans.map((person) => (0, import_react26.createElement)(
++ humans.map((person) => (0, import_react27.createElement)(
+ "div",
+ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
-+ (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Human")),
-+ person.role === "admin" ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
++ (0, import_react27.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
++ (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Human")),
++ person.role === "admin" ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
+ ),
-+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react26.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_react27.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;
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
+ "div",
+ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "button",
+ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
-+ (0, import_react26.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
-+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, member.name),
-+ leads ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
-+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
++ (0, import_react27.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
++ (0, import_react27.createElement)("span", { className: "bt-row-title" }, member.name),
++ leads ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
++ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
+ ),
-+ leads ? (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react26.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
-+ members.length > 2 ? (0, import_react26.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_react27.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
++ members.length > 2 ? (0, import_react27.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_react26.createElement)(
++ remotes.map((person) => (0, import_react27.createElement)(
+ "div",
+ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react26.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react26.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
++ (0, import_react27.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
++ (0, import_react27.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_react26.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_react27.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_react26.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_react27.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_react26.createElement)(
++ ...outsidePeople.map((person) => (0, import_react27.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
-+ (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
++ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
++ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
+ )),
-+ ...outside.map((bot) => (0, import_react26.createElement)(
++ ...outside.map((bot) => (0, import_react27.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_react26.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, bot.name)),
-+ (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add"))
++ (0, import_react27.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, bot.name)),
++ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add"))
+ )),
-+ ...outsideRemotes.map((bot) => (0, import_react26.createElement)(
++ ...outsideRemotes.map((bot) => (0, import_react27.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_react26.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_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
-+ (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
++ (0, import_react27.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_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
++ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
+ )),
-+ (0, import_react26.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_react26.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react26.createElement)(PlusIcon), t("Add member")) : null,
-+ (0, import_react26.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react26.createElement)(
++ (0, import_react27.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_react27.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react27.createElement)(PlusIcon), t("Add member")) : null,
++ (0, import_react27.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react27.createElement)(
+ "span",
+ { className: "bt-set-hint" },
+ // The host checks for two members on every change, even a rename.
@@ -14598,83 +10774,42 @@ 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_react26.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
++ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react27.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ ),
-+ (0, import_react26.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
++ (0, import_react27.createElement)(
+ Card,
+ { title: t("Sidebar") },
-+ (0, import_react26.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_react26.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_react27.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_react27.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_react26.createElement)(
++ (0, import_react27.createElement)(
+ Card,
+ { title: t("More") },
-+ (0, import_react26.createElement)(
++ (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_react26.createElement)(
++ (0, import_react27.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react26.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
-+ (0, import_react26.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_react27.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
++ (0, import_react27.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_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
++ (0, import_react27.createElement)("span", { className: "bt-chevron" }, (0, import_react27.createElement)(ChevronRightIcon))
+ )
+ )
- );
- }
--function AvatarEditor({ bot, actions, run, onClose }) {
-- const look = lookOf(bot);
-- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot");
-- const [over, setOver] = (0, import_react18.useState)(false);
-- const ref = (0, import_react18.useRef)(null);
-- const fileRef = (0, import_react18.useRef)(null);
-- const upload = (file) => {
-- if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } })));
++ );
++}
+function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
-+ const [admin, setAdmin] = (0, import_react26.useState)(null);
-+ const [members, setMembers] = (0, import_react26.useState)([]);
-+ const [name, setName] = (0, import_react26.useState)("");
-+ const [busy, setBusy] = (0, import_react26.useState)(false);
++ const [admin, setAdmin] = (0, import_react27.useState)(null);
++ const [members, setMembers] = (0, import_react27.useState)([]);
++ const [name, setName] = (0, import_react27.useState)("");
++ const [busy, setBusy] = (0, import_react27.useState)(false);
+ const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
+ const pickAdmin = (id) => {
+ setAdmin(id);
+ setMembers((list) => list.filter((item) => item !== id));
- };
-- (0, import_react18.useEffect)(() => {
-- const onDown = (event) => {
-- if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
-- onClose();
-- };
-- const onKey = (event) => {
-- if (event.key === "Escape") {
-- event.stopPropagation();
-- onClose();
-- }
-- };
-- window.addEventListener("mousedown", onDown);
-- window.addEventListener("keydown", onKey, true);
-- return () => {
-- window.removeEventListener("mousedown", onDown);
-- window.removeEventListener("keydown", onKey, true);
-- };
-- }, [onClose]);
-- (0, import_react18.useEffect)(() => {
-- if (tab !== "upload") return void 0;
-- const onPaste = (event) => {
-- const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
-- if (file) {
-- event.preventDefault();
-- upload(file);
-- }
-- };
-- window.addEventListener("paste", onPaste);
-- return () => window.removeEventListener("paste", onPaste);
-- }, [tab, bot]);
-- const hasPhoto = Boolean(look.image);
-- const glide = useGlide(tab);
-- return (0, import_react18.createElement)(
++ };
+ const create = async () => {
+ if (busy || admin === null || members.length === 0) return;
+ setBusy(true);
@@ -14685,129 +10820,51 @@ diff --git a/client.js b/client.js
+ if (done && room2) onCreated(room2);
+ else setBusy(false);
+ };
-+ const chip = (bot, on, onClick, role) => (0, import_react26.createElement)("button", {
++ const chip = (bot, on, onClick, role) => (0, import_react27.createElement)("button", {
+ key: bot.id,
+ type: "button",
+ role,
+ className: "bt-ng-chip",
+ "aria-checked": on,
+ onClick
-+ }, (0, import_react26.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react26.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react26.createElement)(AdminStar) : null);
-+ return (0, import_react26.createElement)(
- "div",
-- { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
-- (0, import_react18.createElement)(
++ }, (0, import_react27.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react27.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react27.createElement)(AdminStar) : null);
++ return (0, import_react27.createElement)(
++ "div",
+ { className: "bt-set-card bt-ng" },
-+ (0, import_react26.createElement)(
- "div",
-- { className: "bt-avedit-top" },
-- (0, import_react18.createElement)(
++ (0, import_react27.createElement)(
++ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react26.createElement)(AdminStar), t("Choose the group admin"))),
-+ (0, import_react26.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_react18.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"))
++ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react27.createElement)(AdminStar), t("Choose the group admin"))),
++ (0, import_react27.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"))
+ )
- ),
-- tab === "bot" ? (0, import_react18.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)(
-+ admin === null ? null : (0, import_react26.createElement)(
- "div",
-- {
-- className: "bt-dropzone",
-- "data-over": over || void 0,
-- onDragOver: (event) => {
-- event.preventDefault();
-- setOver(true);
-- },
-- onDragLeave: () => setOver(false),
-- onDrop: (event) => {
-- event.preventDefault();
-- setOver(false);
-- upload(event.dataTransfer.files?.[0]);
-- }
-- },
-- 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)(
++ ),
++ admin === null ? null : (0, import_react27.createElement)(
++ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
-+ (0, import_react26.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_react18.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);
-- } })
-- ) : null
-- );
--}
--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)(
-- Card,
-- { title: t("Memory") },
-- (0, import_react18.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" }, t("Who to chat with…"))),
++ (0, import_react27.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_react26.createElement)(
++ admin === null || members.length === 0 ? null : (0, import_react27.createElement)(
+ "label",
+ { className: "bt-set-row bt-bs-row bt-ng-step" },
-+ (0, import_react26.createElement)("span", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react26.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
++ (0, import_react27.createElement)("span", { className: "bt-set-copy" }, (0, import_react27.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
++ (0, import_react27.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
+ ),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
+ "div",
+ { className: "bt-set-row bt-task-foot" },
-+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react26.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
- )
- );
- }
--function BotSettingsForm({ bot, roster, actions, run }) {
-- const [editing, setEditing] = (0, import_react18.useState)(false);
-- const [confirming, setConfirming] = (0, import_react18.useState)(false);
-- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []);
-- (0, import_react18.useEffect)(() => {
-- if (!confirming) return void 0;
-- const timer = setTimeout(() => setConfirming(false), 4e3);
-- return () => clearTimeout(timer);
-- }, [confirming]);
-- const isMainBot = isMainOf(roster, bot.id);
-- const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1;
-- const isTile = roster.mainBotId === bot.id;
-- 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);
++ (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ (0, import_react27.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
++ )
++ );
++}
+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}
@@ -14840,13 +10897,13 @@ diff --git a/client.js b/client.js
+`;
+
+// src/client/palette.js
-+var import_react27 = require("react");
++var import_react28 = 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_react27.useState)(start ?? "");
-+ const [cursor, setCursor] = (0, import_react27.useState)(0);
-+ const [leaving, setLeaving] = (0, import_react27.useState)(false);
++ const [query, setQuery] = (0, import_react28.useState)(start ?? "");
++ const [cursor, setCursor] = (0, import_react28.useState)(0);
++ const [leaving, setLeaving] = (0, import_react28.useState)(false);
+ const wanted = query.trim().toLowerCase();
+ const close = () => {
+ if (leaving) return;
@@ -14882,7 +10939,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_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
++ avatar: (0, import_react28.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,
@@ -14893,7 +10950,7 @@ diff --git a/client.js b/client.js
+ })),
+ ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
+ key: room2.id,
-+ avatar: (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: (0, import_react28.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,
@@ -14906,7 +10963,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_react27.createElement)("span", { className: "bt-palette-agent" }, (0, import_react27.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
++ avatar: (0, import_react28.createElement)("span", { className: "bt-palette-agent" }, (0, import_react28.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ title: "DSH Agent",
+ subtitle: t("Plain dsh session"),
+ run: () => actions.openAgent(agent.id)
@@ -14919,195 +10976,94 @@ diff --git a/client.js b/client.js
+ key: `${id}:${turn}`,
+ title: bot?.name ?? room2?.name ?? t("Bot"),
+ subtitle: snippet,
-+ avatar: bot ? (0, import_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: bot ? (0, import_react28.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react28.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_react27.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
-+ { key: "cmd:bot", icon: (0, import_react27.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
-+ { key: "cmd:group", icon: (0, import_react27.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
-+ { key: "cmd:settings", icon: (0, import_react27.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);
-+ const active = Math.min(cursor, Math.max(rows.length - 1, 0));
-+ const shown = rows.map((row) => row.key).join("\n");
-+ const glide = useGlide(`${active}
-+${shown}`, { axis: "y", ...QUICK_EDGES });
-+ const listRef = glide.box;
-+ useFlip(listRef, shown, "[data-flip]");
-+ (0, import_react27.useEffect)(() => {
-+ listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
-+ }, [active, query]);
-+ const choose = (row) => {
-+ if (!row || leaving) return;
-+ void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu"));
-+ close();
- };
-- (0, import_react18.useEffect)(() => {
-- void actions.refreshModels?.();
-- }, [bot.id]);
-- return (0, import_react18.createElement)(
-+ let index = -1;
-+ return (0, import_react27.createElement)(
- "div",
-- { className: "bt-bs" },
-- (0, import_react18.createElement)(
-+ { className: "bt-palette-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react27.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
-+ (0, import_react27.createElement)(
- "div",
-- { className: "bt-bs-hero" },
-- (0, import_react18.createElement)(
-+ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
-+ (0, import_react27.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
-+ { className: "bt-palette-head" },
-+ (0, import_react27.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react27.createElement)(SearchIcon)),
-+ (0, import_react27.createElement)("input", {
-+ autoFocus: true,
-+ value: query,
-+ placeholder: t("Search Bots, chats and messages"),
-+ "aria-label": t("Search"),
-+ role: "combobox",
-+ "aria-expanded": true,
-+ "aria-controls": "bt-palette-list",
-+ onChange: (event) => {
-+ setQuery(event.target.value);
-+ setCursor(0);
-+ },
-+ onKeyDown: (event) => {
-+ if (event.nativeEvent.isComposing) return;
-+ if (event.key === "ArrowDown") {
-+ event.preventDefault();
-+ setCursor(Math.min(active + 1, rows.length - 1));
-+ } else if (event.key === "ArrowUp") {
-+ event.preventDefault();
-+ setCursor(Math.max(active - 1, 0));
-+ } else if (event.key === "Enter") {
-+ event.preventDefault();
-+ choose(rows[active]);
-+ }
-+ }
-+ }),
-+ query ? (0, import_react27.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
-+ setQuery("");
-+ setCursor(0);
-+ } }, (0, import_react27.createElement)(CloseIcon)) : null
- ),
-- (0, import_react18.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_react18.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_react18.createElement)(
-- Card,
-- { title: t("Model") },
-- (0, import_react18.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_react18.createElement)(
-- Card,
-- { title: t("Working folder") },
-- (0, import_react18.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_react18.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 }) })
-- )
-- ),
-- (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)(
-- Card,
-- { title: t("Main Bot") },
-- onlyMain ? (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("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))
-- ),
-- 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))
++ });
++ if (messages.length) sections.push([t("Messages"), messages]);
++ const commands = [
++ { key: "cmd:new", icon: (0, import_react28.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
++ { key: "cmd:bot", icon: (0, import_react28.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
++ { key: "cmd:group", icon: (0, import_react28.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
++ { key: "cmd:settings", icon: (0, import_react28.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);
++ const active = Math.min(cursor, Math.max(rows.length - 1, 0));
++ const shown = rows.map((row) => row.key).join("\n");
++ const glide = useGlide(`${active}
++${shown}`, { axis: "y", ...QUICK_EDGES });
++ const listRef = glide.box;
++ useFlip(listRef, shown, "[data-flip]");
++ (0, import_react28.useEffect)(() => {
++ listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
++ }, [active, query]);
++ const choose = (row) => {
++ if (!row || leaving) return;
++ void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu"));
++ close();
++ };
++ let index = -1;
++ return (0, import_react28.createElement)(
++ "div",
++ { className: "bt-palette-layer", "data-leaving": leaving || void 0 },
++ (0, import_react28.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
++ (0, import_react28.createElement)(
++ "div",
++ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
++ (0, import_react28.createElement)(
++ "div",
++ { className: "bt-palette-head" },
++ (0, import_react28.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react28.createElement)(SearchIcon)),
++ (0, import_react28.createElement)("input", {
++ autoFocus: true,
++ value: query,
++ placeholder: t("Search Bots, chats and messages"),
++ "aria-label": t("Search"),
++ role: "combobox",
++ "aria-expanded": true,
++ "aria-controls": "bt-palette-list",
++ onChange: (event) => {
++ setQuery(event.target.value);
++ setCursor(0);
++ },
++ onKeyDown: (event) => {
++ if (event.nativeEvent.isComposing) return;
++ if (event.key === "ArrowDown") {
++ event.preventDefault();
++ setCursor(Math.min(active + 1, rows.length - 1));
++ } else if (event.key === "ArrowUp") {
++ event.preventDefault();
++ setCursor(Math.max(active - 1, 0));
++ } else if (event.key === "Enter") {
++ event.preventDefault();
++ choose(rows[active]);
++ }
++ }
++ }),
++ query ? (0, import_react28.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
++ setQuery("");
++ setCursor(0);
++ } }, (0, import_react28.createElement)(CloseIcon)) : null
++ ),
++ (0, import_react28.createElement)(
++ "div",
+ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
-+ (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ rows.length === 0 ? (0, import_react27.createElement)(
++ (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ rows.length === 0 ? (0, import_react28.createElement)(
+ "div",
+ { className: "bt-palette-empty" },
-+ (0, import_react27.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react27.createElement)(SearchIcon)),
-+ (0, import_react27.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
-+ (0, import_react27.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
-+ ) : sections.map(([title, items]) => (0, import_react27.createElement)(
-+ import_react27.Fragment,
++ (0, import_react28.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react28.createElement)(SearchIcon)),
++ (0, import_react28.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
++ (0, import_react28.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
++ ) : sections.map(([title, items]) => (0, import_react28.createElement)(
++ import_react28.Fragment,
+ { key: title },
-+ (0, import_react27.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
++ (0, import_react28.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ items.map((row) => {
+ index += 1;
+ const at = index;
-+ return (0, import_react27.createElement)(
++ return (0, import_react28.createElement)(
+ "button",
+ {
+ key: row.key,
@@ -15121,58 +11077,26 @@ diff --git a/client.js b/client.js
+ },
+ onClick: () => choose(row)
+ },
-+ (0, import_react27.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react27.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react28.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
++ (0, import_react28.createElement)(
+ "span",
+ { className: "bt-palette-text" },
-+ (0, import_react27.createElement)("span", { className: "bt-palette-title" }, row.title),
-+ row.subtitle ? (0, import_react27.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
++ (0, import_react28.createElement)("span", { className: "bt-palette-title" }, row.title),
++ row.subtitle ? (0, import_react28.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
+ ),
-+ row.badge ? (0, import_react27.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
++ row.badge ? (0, import_react28.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") {
-@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") {
+ const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase()));
+@@ -8165,36 +12759,48 @@
}
function startModel(roster) {
const models = roster.models ?? [];
@@ -15203,7 +11127,7 @@ diff --git a/client.js b/client.js
// src/client/create-bot.js
-var import_react19 = require("react");
-+var import_react28 = require("react");
++var import_react29 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -15221,22 +11145,22 @@ 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_react28.useState)(() => randomLook());
-+ const [image, setImage] = (0, import_react28.useState)(null);
-+ const [name, setName] = (0, import_react28.useState)(start?.name ?? "");
-+ const [model, setModel] = (0, import_react28.useState)(() => startModel(roster));
-+ const [effort, setEffort] = (0, import_react28.useState)(void 0);
-+ const [busy, setBusy] = (0, import_react28.useState)(false);
-+ const [error, setError] = (0, import_react28.useState)("");
-+ const fileRef = (0, import_react28.useRef)(null);
-+ const frame = (0, import_react28.useRef)(null);
++ const [look, setLook] = (0, import_react29.useState)(() => randomLook());
++ const [image, setImage] = (0, import_react29.useState)(null);
++ const [name, setName] = (0, import_react29.useState)(start?.name ?? "");
++ const [model, setModel] = (0, import_react29.useState)(() => startModel(roster));
++ const [effort, setEffort] = (0, import_react29.useState)(void 0);
++ const [busy, setBusy] = (0, import_react29.useState)(false);
++ const [error, setError] = (0, import_react29.useState)("");
++ const fileRef = (0, import_react29.useRef)(null);
++ const frame = (0, import_react29.useRef)(null);
useEscape(onBack);
- (0, import_react19.useEffect)(() => {
-+ (0, import_react28.useEffect)(() => {
++ (0, import_react29.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
-@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
+@@ -8206,8 +12812,8 @@
setError("");
setBusy(true);
try {
@@ -15247,59 +11171,59 @@ diff --git a/client.js b/client.js
} catch (failure) {
setError(failure?.message ?? String(failure));
setBusy(false);
-@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
+@@ -8219,25 +12825,25 @@
avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure)));
};
const previewLook = image ? { ...look, image } : look;
- const card = (0, import_react19.createElement)(
-+ const card = (0, import_react28.createElement)(
++ const card = (0, import_react29.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-create-name" }, finalName)
++ (0, import_react29.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)("div", { className: "bt-create-step" }, t("Look")),
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)("div", { className: "bt-create-step" }, t("Look")),
++ (0, import_react29.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react19.createElement)(CharacterGrid, {
-+ (0, import_react28.createElement)(CharacterGrid, {
++ (0, import_react29.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
-@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
+@@ -8249,27 +12855,27 @@
setLook((current) => ({ ...current, color }));
}
}),
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
-+ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
++ (0, import_react29.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_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
-+ image ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
-+ (0, import_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
++ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
++ image ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
++ (0, import_react29.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);
@@ -15307,76 +11231,76 @@ diff --git a/client.js b/client.js
)
),
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react19.createElement)("input", {
-+ (0, import_react28.createElement)("span", { className: "bt-field-label" }, t("Name")),
-+ (0, import_react28.createElement)("input", {
++ (0, import_react29.createElement)("span", { className: "bt-field-label" }, t("Name")),
++ (0, import_react29.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
-@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
+@@ -8284,75 +12890,113 @@
}
}
}),
- 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_react28.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react28.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
++ problem ? (0, import_react29.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react29.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
),
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-field-label" }, t("Model")),
-+ (0, import_react28.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
++ (0, import_react29.createElement)("span", { className: "bt-field-label" }, t("Model")),
++ (0, import_react29.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_react28.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_react29.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_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
-+ (0, import_react28.createElement)(
++ error ? (0, import_react29.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
++ (0, import_react29.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_react28.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
-+ (0, import_react28.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_react29.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
++ (0, import_react29.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_react28.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
-+ return (0, import_react28.createElement)(
++ if (inline) return (0, import_react29.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
++ return (0, import_react29.createElement)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react19.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react28.createElement)(ChevronLeftIcon)) : (0, import_react28.createElement)("span", { className: "bt-create-gap" }),
-+ (0, import_react28.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
-+ (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react28.createElement)(CloseIcon))
++ canGoBack ? (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react29.createElement)(ChevronLeftIcon)) : (0, import_react29.createElement)("span", { className: "bt-create-gap" }),
++ (0, import_react29.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
++ (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react29.createElement)(CloseIcon))
),
- (0, import_react19.createElement)("div", { className: "bt-create-body" }, card)
-+ (0, import_react28.createElement)("div", { className: "bt-create-body" }, card)
++ (0, import_react29.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_react29 = require("react");
++var import_react30 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react20.useState)("");
- const [picked, setPicked] = (0, import_react20.useState)([]);
@@ -15388,20 +11312,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_react29.useState)("");
-+ const [picked, setPicked] = (0, import_react29.useState)([]);
-+ const [group, setGroup] = (0, import_react29.useState)(mode === "group");
-+ const [compose, setCompose] = (0, import_react29.useState)(mode !== "new");
-+ const [admin, setAdmin] = (0, import_react29.useState)(null);
-+ const [message, setMessage] = (0, import_react29.useState)("");
-+ const [busy, setBusy] = (0, import_react29.useState)(false);
-+ const [error, setError] = (0, import_react29.useState)("");
-+ const [fix, setFix] = (0, import_react29.useState)(null);
-+ const [fixNote, setFixNote] = (0, import_react29.useState)("");
-+ const [inviteLink, setInviteLink] = (0, import_react29.useState)("");
-+ const [cursor, setCursor] = (0, import_react29.useState)(0);
-+ const [draft, setDraft] = (0, import_react29.useState)(mode === "create" ? { name: "", fromList: false } : null);
-+ const inputRef = (0, import_react29.useRef)(null);
++ const [query, setQuery] = (0, import_react30.useState)("");
++ const [picked, setPicked] = (0, import_react30.useState)([]);
++ const [group, setGroup] = (0, import_react30.useState)(mode === "group");
++ const [compose, setCompose] = (0, import_react30.useState)(mode !== "new");
++ const [admin, setAdmin] = (0, import_react30.useState)(null);
++ const [message, setMessage] = (0, import_react30.useState)("");
++ const [busy, setBusy] = (0, import_react30.useState)(false);
++ const [error, setError] = (0, import_react30.useState)("");
++ const [fix, setFix] = (0, import_react30.useState)(null);
++ const [fixNote, setFixNote] = (0, import_react30.useState)("");
++ const [inviteLink, setInviteLink] = (0, import_react30.useState)("");
++ const [cursor, setCursor] = (0, import_react30.useState)(0);
++ const [draft, setDraft] = (0, import_react30.useState)(mode === "create" ? { name: "", fromList: false } : null);
++ const inputRef = (0, import_react30.useRef)(null);
const close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -15461,7 +11385,7 @@ diff --git a/client.js b/client.js
const pick2 = (bot) => {
setPicked((current) => [...current, bot.id]);
setQuery("");
-@@ -8386,13 +8386,17 @@ function NewChat({ mode, roster, actions }) {
+@@ -8386,13 +13030,17 @@
};
const created = (bot) => {
close();
@@ -15482,7 +11406,7 @@ diff --git a/client.js b/client.js
if (text) await actions.send(room2.id, text);
actions.openSession(room2.id);
close();
-@@ -8411,46 +8411,75 @@ function NewChat({ mode, roster, actions }) {
+@@ -8411,46 +13059,75 @@
close();
return;
}
@@ -15503,14 +11427,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_react29.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
++ options.push({ key: "chat", icon: (0, import_react30.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_react29.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_react29.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
++ options.push({ key: "new", icon: (0, import_react30.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_react30.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
setGroup(true);
+ setCompose(true);
setCursor(0);
@@ -15519,7 +11443,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_react29.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react29.createElement)(PeopleIcon) }) : (0, import_react29.createElement)(PeopleIcon),
++ icon: action.icon ? (0, import_react30.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react30.createElement)(PeopleIcon) }) : (0, import_react30.createElement)(PeopleIcon),
+ label: t(action.label),
+ hint: action.hint ? t(action.hint) : void 0,
+ run: () => {
@@ -15532,7 +11456,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_react29.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_react30.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -15546,22 +11470,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_react29.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot });
++ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react30.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_react29.useRef)(null);
++ const drop = (0, import_react30.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react20.createElement)(
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(CreateBot, {
-+ (0, import_react29.createElement)(CreateBot, {
++ (0, import_react30.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -15569,12 +11493,12 @@ diff --git a/client.js b/client.js
canGoBack: draft.fromList,
onBack: draft.fromList ? () => {
setDraft(null);
-@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
+@@ -8467,33 +13144,41 @@
if (picked.length > 0) setPicked((current) => current.slice(0, -1));
else if (admin !== null) setAdmin(null);
};
- return (0, import_react20.createElement)(
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
"div",
- { className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(
@@ -15586,52 +11510,52 @@ diff --git a/client.js b/client.js
+ if (event.target === event.currentTarget) close();
+ } : void 0
+ },
-+ (0, import_react29.createElement)(
++ (0, import_react30.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_react29.createElement)(
++ menuOnly ? null : (0, import_react30.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react20.createElement)(
-+ (0, import_react29.createElement)("span", { className: "bt-to-label" }, t("To:")),
-+ adminBot ? (0, import_react29.createElement)(
++ (0, import_react30.createElement)("span", { className: "bt-to-label" }, t("To:")),
++ adminBot ? (0, import_react30.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_react29.createElement)(BotMark, { bot: adminBot, size: 16 }),
++ (0, import_react30.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_react29.createElement)(AdminStar),
-+ (0, import_react29.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
++ (0, import_react30.createElement)(AdminStar),
++ (0, import_react30.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_react29.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react29.createElement)(AdminStar)) : null,
-+ pickedBots.map((bot) => (0, import_react29.createElement)(
++ choosingAdmin ? (0, import_react30.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react30.createElement)(AdminStar)) : null,
++ pickedBots.map((bot) => (0, import_react30.createElement)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react20.createElement)(BotMark, { bot, size: 16 }),
-+ (0, import_react29.createElement)(BotMark, { bot, size: 16 }),
++ (0, import_react30.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_react29.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
++ (0, import_react30.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_react29.createElement)("input", {
++ (0, import_react30.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
-@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
+@@ -8517,20 +13202,20 @@
} else if (event.key === "Backspace" && query === "") unpick();
}
}),
- group && admin !== null ? (0, import_react20.createElement)("button", {
-+ group && admin !== null ? (0, import_react29.createElement)("button", {
++ group && admin !== null ? (0, import_react30.createElement)("button", {
type: "button",
className: "bt-send bt-to-create",
disabled: busy || picked.length === 0,
@@ -15639,86 +11563,86 @@ 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_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
++ (0, import_react30.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_react29.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react29.createElement)(
++ (0, import_react30.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react30.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_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.length === 0 ? (0, import_react29.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_react29.createElement)(
++ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ options.length === 0 ? (0, import_react30.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_react30.createElement)(
"button",
{
key: option.key,
-@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
+@@ -8542,21 +13227,36 @@
onMouseEnter: () => setCursor(index),
onClick: option.run
},
- 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_react29.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react29.createElement)("span", { className: "bt-option-icon" }, option.icon),
++ option.bot ? (0, import_react30.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react30.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_react29.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react29.createElement)("span", { className: "bt-option-copy" }, (0, import_react29.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react29.createElement)("span", { className: "bt-option-sub" }, option.hint)),
-+ option.bot && option.hint && index === active ? (0, import_react29.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
++ option.bot || !option.hint ? (0, import_react30.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react30.createElement)("span", { className: "bt-option-copy" }, (0, import_react30.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react30.createElement)("span", { className: "bt-option-sub" }, option.hint)),
++ option.bot && option.hint && index === active ? (0, import_react30.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_react29.createElement)("div", { className: "bt-nc-spacer" }),
-+ error && fix ? (0, import_react29.createElement)(
++ menuOnly ? null : (0, import_react30.createElement)("div", { className: "bt-nc-spacer" }),
++ error && fix ? (0, import_react30.createElement)(
+ "div",
+ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
-+ (0, import_react29.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_react29.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_react29.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
++ (0, import_react30.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_react30.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_react30.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_react29.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_react30.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_react29.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
-+ inviteLink ? (0, import_react29.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_react29.createElement)("div", { className: "bt-error" }, error) : null,
-+ menuOnly ? null : (0, import_react29.createElement)(
++ fixNote ? (0, import_react30.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
++ inviteLink ? (0, import_react30.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_react30.createElement)("div", { className: "bt-error" }, error) : null,
++ menuOnly ? null : (0, import_react30.createElement)(
"div",
{ className: "bt-nc-compose" },
- (0, import_react20.createElement)(
-+ (0, import_react29.createElement)(
++ (0, import_react30.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_react29.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react29.createElement)(PlusIcon)),
-+ (0, import_react29.createElement)("textarea", {
++ (0, import_react30.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react30.createElement)(PlusIcon)),
++ (0, import_react30.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
-@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
+@@ -8569,7 +13269,7 @@
}
}
}),
- (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_react29.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react29.createElement)(ArrowUpIcon))
++ (0, import_react30.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react30.createElement)(ArrowUpIcon))
)
)
)
-@@ -8577,18 +8577,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
+@@ -8577,18 +13277,18 @@
}
// src/client/exchange.js
-var import_react21 = require("react");
-+var import_react30 = require("react");
++var import_react31 = 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_react30.useState)(null);
-+ const [leaving, setLeaving] = (0, import_react30.useState)(false);
++ const [entries2, setEntries] = (0, import_react31.useState)(null);
++ const [leaving, setLeaving] = (0, import_react31.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
@@ -15726,83 +11650,83 @@ diff --git a/client.js b/client.js
};
useEscape(close);
- (0, import_react21.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
-@@ -8612,48 +8612,48 @@ function ExchangeDialog({ pair, roster, actions }) {
+@@ -8612,48 +13312,48 @@
const right = roster.byId[pair[1]];
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react21.createElement)(
-+ return (0, import_react30.createElement)(
++ return (0, import_react31.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react21.createElement)(
-+ (0, import_react30.createElement)(
++ (0, import_react31.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_react30.createElement)("div", { className: "bt-head" }, (0, import_react30.createElement)(
++ (0, import_react31.createElement)("div", { className: "bt-head" }, (0, import_react31.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null,
-+ left ? (0, import_react30.createElement)(BotMark, { bot: left, size: 22 }) : null,
++ left ? (0, import_react31.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_react30.createElement)("span", { className: "bt-swap" }, "⇄"),
-+ right ? (0, import_react30.createElement)(BotMark, { bot: right, size: 22 }) : null,
++ (0, import_react31.createElement)("span", { className: "bt-swap" }, "⇄"),
++ right ? (0, import_react31.createElement)(BotMark, { bot: right, size: 22 }) : null,
right?.name ?? "?"
)),
- (0, import_react21.createElement)(
-+ (0, import_react30.createElement)(
++ (0, import_react31.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_react30.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react30.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
++ entries2 === null ? (0, import_react31.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react31.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_react30.createElement)(
-+ import_react30.Fragment,
++ return (0, import_react31.createElement)(
++ import_react31.Fragment,
{ key: entry.id },
- showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react21.createElement)(
-+ showDay ? (0, import_react30.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
-+ (0, import_react30.createElement)(
++ showDay ? (0, import_react31.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
++ (0, import_react31.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_react30.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
-+ (0, import_react30.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react30.createElement)("span", { className: "bt-lead" }, (0, import_react30.createElement)(BotMark, { bot, size: 22 })) })
++ (0, import_react31.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
++ (0, import_react31.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react31.createElement)("span", { className: "bt-lead" }, (0, import_react31.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_react30.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react30.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
++ (0, import_react31.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
)
);
}
// src/client/voice.js
-var import_react22 = require("react");
-+var import_react31 = require("react");
++var import_react32 = 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",
-@@ -8665,14 +8665,14 @@ var VOICE_LABELS = {
+@@ -8665,14 +13365,14 @@
};
var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" };
function useMicLevel(enabled) {
- const [level, setLevel] = (0, import_react22.useState)(0);
- (0, import_react22.useEffect)(() => {
-+ const [level, setLevel] = (0, import_react31.useState)(0);
-+ (0, import_react31.useEffect)(() => {
++ const [level, setLevel] = (0, import_react32.useState)(0);
++ (0, import_react32.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -15813,7 +11737,7 @@ diff --git a/client.js b/client.js
if (stopped) {
media.getTracks().forEach((track) => track.stop());
return;
-@@ -8716,18 +8716,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
+@@ -8716,18 +13416,18 @@
const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0;
return entry ? `${outline.length}|${entry.response ?? ""}` : "0|";
});
@@ -15827,181 +11751,129 @@ 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_react31.useState)(() => actions.speech());
-+ const [phase, setPhase] = (0, import_react31.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
-+ const [catalog, setCatalog] = (0, import_react31.useState)(null);
-+ const [heard, setHeard] = (0, import_react31.useState)("");
-+ const [said, setSaid] = (0, import_react31.useState)("");
-+ const [note, setNote] = (0, import_react31.useState)("");
-+ const [serviceLevel, setServiceLevel] = (0, import_react31.useState)(0);
-+ const [cycle, setCycle] = (0, import_react31.useState)(0);
-+ const waiting = (0, import_react31.useRef)(null);
-+ const reply = (0, import_react31.useRef)(lastReply);
++ const [speech] = (0, import_react32.useState)(() => actions.speech());
++ const [phase, setPhase] = (0, import_react32.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
++ const [catalog, setCatalog] = (0, import_react32.useState)(null);
++ const [heard, setHeard] = (0, import_react32.useState)("");
++ const [said, setSaid] = (0, import_react32.useState)("");
++ const [note, setNote] = (0, import_react32.useState)("");
++ const [serviceLevel, setServiceLevel] = (0, import_react32.useState)(0);
++ const [cycle, setCycle] = (0, import_react32.useState)(0);
++ const waiting = (0, import_react32.useRef)(null);
++ const reply = (0, import_react32.useRef)(lastReply);
reply.current = lastReply;
- const turn = (0, import_react22.useRef)(0);
-+ const turn = (0, import_react31.useRef)(0);
++ const turn = (0, import_react32.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
-@@ -8747,12 +8747,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
+@@ -8747,12 +13447,12 @@
dictation.stop();
submit(text);
});
- (0, import_react22.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
-@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
+@@ -8767,16 +13467,16 @@
};
}, [speech]);
const listen = phase === "listening";
- (0, import_react22.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.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_react31.useEffect)(() => {
++ (0, import_react32.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
-@@ -8791,375 +8791,131 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
- const wave = await current.finish(catalog.maxDurationSeconds);
- const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : "";
- if (turn.current !== own) return;
-- if (text) submit(text);
-- else setPhase("listening");
-- } catch (error) {
-- if (turn.current !== own) return;
-- setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
-- setPhase("listening");
-- }
-- };
-- openCapture().then((opened) => {
-- if (stopped) {
-- opened.cancel();
-- return;
-- }
-- capture = opened;
-- const began = performance.now();
-- const tick = () => {
-- const now = performance.now();
-- const level2 = capture.level();
-- smooth = smooth * 0.8 + level2 * 0.2;
-- setServiceLevel(Math.round(smooth * 100) / 100);
-- const state = detect(level2, now);
-- if (state === "done" || state === "speech" && now - began >= limit) {
-- const current = capture;
-- capture = null;
-- setServiceLevel(0);
-- void finish(current);
-- return;
-- }
-- if (state === "waiting" && now - began >= limit) {
-- setCycle((value) => value + 1);
-- return;
-- }
-- frame = requestAnimationFrame(tick);
-- };
-- tick();
-- }, (error) => {
-- if (stopped) return;
-- setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
-- setPhase("paused");
-- });
-- return () => {
-- stopped = true;
-- cancelAnimationFrame(frame);
-- capture?.cancel();
-- setServiceLevel(0);
-- };
-- }, [speech, catalog, listen, cycle]);
+@@ -8838,7 +13538,7 @@
+ setServiceLevel(0);
+ };
+ }, [speech, catalog, listen, cycle]);
- (0, import_react22.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;
-- waiting.current = null;
-- const spoken = plainSpeech(text);
-- setSaid(spoken);
-- setHeard("");
-- if (!window.speechSynthesis || !spoken) {
-- setPhase("listening");
-- return;
-- }
-- const utterance = new SpeechSynthesisUtterance(spoken);
-- utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US";
-- utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current);
-- utterance.onerror = utterance.onend;
-- setPhase("speaking");
-- window.speechSynthesis.cancel();
-- window.speechSynthesis.speak(utterance);
-- }, [phase, running, lastReply]);
++ (0, import_react32.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 +13558,7 @@
+ window.speechSynthesis.cancel();
+ window.speechSynthesis.speak(utterance);
+ }, [phase, running, lastReply]);
- (0, import_react22.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";
-- 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;
++ (0, import_react32.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,291 +13575,47 @@
+ const openSetup = phase === "setup" ? actions.voiceSetup() : null;
+ const caption = phase === "speaking" ? said : heard;
+ const shownNote = note || dictation.note;
- return (0, import_react22.createElement)(
-- "div",
-- { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
++ return (0, import_react32.createElement)(
+ "div",
+ { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react22.createElement)(
-- "div",
-- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
++ (0, import_react32.createElement)(
+ "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)(
-- "div",
-- { className: "bt-voice-orb", style: { "--bt-level": level } },
++ (0, import_react32.createElement)(
+ "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_react32.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
++ (0, import_react32.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
++ bot ? (0, import_react32.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react32.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
+ ),
- (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react22.createElement)(
-- "div",
-- { className: "bt-voice-status", role: "status" },
++ (0, import_react32.createElement)("div", { className: "bt-voice-name" }, name),
++ (0, import_react32.createElement)(
+ "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_react32.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react32.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: () => {
-- close();
-- openSetup();
-- } }, t("Set up voice input")) : null,
++ (0, import_react32.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
++ openSetup ? (0, import_react32.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
+ close();
+ openSetup();
+ } }, t("Set up voice input")) : null,
- shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
-- ),
++ shownNote ? (0, import_react32.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
+ ),
- (0, import_react22.createElement)(
-- "div",
-- { className: "bt-voice-bar" },
++ (0, import_react32.createElement)(
+ "div",
+ { className: "bt-voice-bar" },
- (0, import_react22.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_react32.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_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))
- )
@@ -16162,9 +12034,11 @@ diff --git a/client.js b/client.js
- ),
- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon))
- )
-- )
-- );
--}
++ }, (0, import_react32.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react32.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
++ (0, import_react32.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react32.createElement)(CloseIcon))
+ )
+ );
+ }
-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
- const [admin, setAdmin] = (0, import_react23.useState)(null);
- const [members, setMembers] = (0, import_react23.useState)([]);
@@ -16184,87 +12058,7 @@ diff --git a/client.js b/client.js
- }));
- if (done && room2) onCreated(room2);
- else setBusy(false);
-+ if (text) submit(text);
-+ else setPhase("listening");
-+ } catch (error) {
-+ if (turn.current !== own) return;
-+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
-+ setPhase("listening");
-+ }
-+ };
-+ openCapture().then((opened) => {
-+ if (stopped) {
-+ opened.cancel();
-+ return;
-+ }
-+ capture = opened;
-+ const began = performance.now();
-+ const tick = () => {
-+ const now = performance.now();
-+ const level2 = capture.level();
-+ smooth = smooth * 0.8 + level2 * 0.2;
-+ setServiceLevel(Math.round(smooth * 100) / 100);
-+ const state = detect(level2, now);
-+ if (state === "done" || state === "speech" && now - began >= limit) {
-+ const current = capture;
-+ capture = null;
-+ setServiceLevel(0);
-+ void finish(current);
-+ return;
-+ }
-+ if (state === "waiting" && now - began >= limit) {
-+ setCycle((value) => value + 1);
-+ return;
-+ }
-+ frame = requestAnimationFrame(tick);
-+ };
-+ tick();
-+ }, (error) => {
-+ if (stopped) return;
-+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
-+ setPhase("paused");
-+ });
-+ return () => {
-+ stopped = true;
-+ cancelAnimationFrame(frame);
-+ capture?.cancel();
-+ setServiceLevel(0);
-+ };
-+ }, [speech, catalog, listen, cycle]);
-+ (0, import_react31.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;
-+ waiting.current = null;
-+ const spoken = plainSpeech(text);
-+ setSaid(spoken);
-+ setHeard("");
-+ if (!window.speechSynthesis || !spoken) {
-+ setPhase("listening");
-+ return;
-+ }
-+ const utterance = new SpeechSynthesisUtterance(spoken);
-+ utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US";
-+ utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current);
-+ utterance.onerror = utterance.onend;
-+ setPhase("speaking");
-+ window.speechSynthesis.cancel();
-+ window.speechSynthesis.speak(utterance);
-+ }, [phase, running, lastReply]);
-+ (0, import_react31.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";
-+ 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 chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", {
- key: bot.id,
- type: "button",
@@ -16274,22 +12068,14 @@ diff --git a/client.js b/client.js
- onClick
- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null);
- return (0, import_react23.createElement)(
-+ const openSetup = phase === "setup" ? actions.voiceSetup() : null;
-+ const caption = phase === "speaking" ? said : heard;
-+ const shownNote = note || dictation.note;
-+ return (0, import_react31.createElement)(
- "div",
+- "div",
- { className: "bt-set-card bt-ng" },
- (0, import_react23.createElement)(
-+ { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
-+ (0, import_react31.createElement)(
- "div",
+- "div",
- { className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react23.createElement)(
-+ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
-+ (0, import_react31.createElement)(
- "div",
+- "div",
- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") },
- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio"))
- )
@@ -16299,14 +12085,7 @@ diff --git a/client.js b/client.js
- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
- (0, import_react23.createElement)(
-+ { className: "bt-voice-orb", style: { "--bt-level": level } },
-+ (0, import_react31.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
-+ (0, import_react31.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
-+ bot ? (0, import_react31.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
-+ ),
-+ (0, import_react31.createElement)("div", { className: "bt-voice-name" }, name),
-+ (0, import_react31.createElement)(
- "div",
+- "div",
- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") },
- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox"))
- )
@@ -16316,35 +12095,15 @@ 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_react31.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react31.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
-+ ),
-+ (0, import_react31.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
-+ openSetup ? (0, import_react31.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
-+ close();
-+ openSetup();
-+ } }, t("Set up voice input")) : null,
-+ shownNote ? (0, import_react31.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
- ),
+- ),
- (0, import_react23.createElement)(
-+ (0, import_react31.createElement)(
- "div",
+- "div",
- { className: "bt-set-row bt-task-foot" },
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
-+ { className: "bt-voice-bar" },
-+ (0, import_react31.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_react31.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react31.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
-+ (0, import_react31.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react31.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}
@@ -16369,7 +12128,7 @@ diff --git a/client.js b/client.js
// src/client/usage-data.js
var HOUR = 0;
-@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") {
+@@ -9277,13 +13733,13 @@
}
return result;
}
@@ -16385,12 +12144,12 @@ diff --git a/client.js b/client.js
}
return String(Math.round(count));
}
-@@ -9294,11 +9294,11 @@ function percent(part, whole) {
+@@ -9294,11 +13750,11 @@
}
// src/client/usage-page.js
-var import_react24 = require("react");
-+var import_react32 = require("react");
++var import_react33 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -16399,61 +12158,61 @@ diff --git a/client.js b/client.js
var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id];
var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit];
var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"];
-@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) {
+@@ -9341,9 +13797,9 @@
return dates({ year: "numeric", month: "long" }).format(bucket.start);
}
function useTween(value) {
- const [shown, setShown] = (0, import_react24.useState)(0);
- const from = (0, import_react24.useRef)(0);
- (0, import_react24.useEffect)(() => {
-+ const [shown, setShown] = (0, import_react32.useState)(0);
-+ const from = (0, import_react32.useRef)(0);
-+ (0, import_react32.useEffect)(() => {
++ const [shown, setShown] = (0, import_react33.useState)(0);
++ const from = (0, import_react33.useRef)(0);
++ (0, import_react33.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
-@@ -9363,9 +9363,9 @@ function useTween(value) {
+@@ -9363,9 +13819,9 @@
}, [value]);
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_react32.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react32.createElement)(icon3));
++var glyphBox = (size, icon3) => (0, import_react33.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react33.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react24.createElement)("span", {
-+ return (0, import_react32.createElement)("span", {
++ return (0, import_react33.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))` }
-@@ -9382,7 +9382,7 @@ function ownerFace(owner, roster) {
+@@ -9382,7 +13838,7 @@
sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role,
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_react32.createElement)(BotAvatar, { bot, size, badge: false, live: false })
++ glyph: (size) => (0, import_react33.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
-@@ -9392,7 +9392,7 @@ function ownerFace(owner, roster) {
+@@ -9392,7 +13848,7 @@
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_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
-+ glyph: (size) => room2 ? (0, import_react32.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
++ glyph: (size) => room2 ? (0, import_react33.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
-@@ -9414,30 +9414,30 @@ function Tabs({ label, value, options, onChange, boxRef }) {
+@@ -9414,30 +13870,30 @@
const names = options.map(([, text]) => text).join("\n");
const glide = useGlide(`${value}
${names}`, { box: boxRef });
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.map(([id, text, icon3]) => (0, import_react32.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react32.createElement)(icon3) : null, text))
++ (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ options.map(([id, text, icon3]) => (0, import_react33.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react33.createElement)(icon3) : null, text))
);
}
function Stats({ range, total, previous }) {
@@ -16461,34 +12220,34 @@ 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_react32.createElement)("div", { className: "bt-us-stat" }, (0, import_react32.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react32.createElement)("span", { className: "bt-us-stat-value" }, value));
-+ return (0, import_react32.createElement)(
++ const cell = (label, value) => (0, import_react33.createElement)("div", { className: "bt-us-stat" }, (0, import_react33.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react33.createElement)("span", { className: "bt-us-stat-value" }, value));
++ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
++ (0, import_react33.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_react32.createElement)("small", null, t("tokens")),
-+ change === null ? null : (0, import_react32.createElement)(
++ (0, import_react33.createElement)("small", null, t("tokens")),
++ change === null ? null : (0, import_react33.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
-@@ -9455,83 +9455,83 @@ function niceTop(max) {
+@@ -9455,83 +13911,83 @@
return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max);
}
function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) {
- const [hover, setHover] = (0, import_react24.useState)(null);
-+ const [hover, setHover] = (0, import_react32.useState)(null);
++ const [hover, setHover] = (0, import_react33.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]));
@@ -16496,77 +12255,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_react32.createElement)(
++ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("h3", null, t("Tokens over time"), (0, import_react32.createElement)("span", null, unitLabel(range.unit))),
-+ onBy ? (0, import_react32.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
++ (0, import_react33.createElement)("h3", null, t("Tokens over time"), (0, import_react33.createElement)("span", null, unitLabel(range.unit))),
++ onBy ? (0, import_react33.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_react32.createElement)(
++ (0, import_react33.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react32.createElement)("span", null, compact(top * level)))),
-+ (0, import_react32.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react32.createElement)("span", null, "0")),
-+ (0, import_react32.createElement)(
++ [1, 0.5].map((level) => (0, import_react33.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react33.createElement)("span", null, compact(top * level)))),
++ (0, import_react33.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react33.createElement)("span", null, "0")),
++ (0, import_react33.createElement)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react24.createElement)("div", {
-+ columns.map((column, at) => (0, import_react32.createElement)("div", {
++ columns.map((column, at) => (0, import_react33.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_react32.createElement)(
++ }, column.total > 0 ? (0, import_react33.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_react32.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
-+ ) : (0, import_react32.createElement)("div", { className: "bt-us-stub" })))
++ column.parts.map(([key, value]) => value > 0 ? (0, import_react33.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
++ ) : (0, import_react33.createElement)("div", { className: "bt-us-stub" })))
),
- tip ? (0, import_react24.createElement)(
-+ tip ? (0, import_react32.createElement)(
++ tip ? (0, import_react33.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_react32.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
-+ (0, import_react32.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_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
++ (0, import_react33.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_react33.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_react32.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
-+ (0, import_react32.createElement)("span", null, tipOf.get(key)?.name),
-+ (0, import_react32.createElement)("b", null, compact(value))
++ (0, import_react33.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
++ (0, import_react33.createElement)("span", null, tipOf.get(key)?.name),
++ (0, import_react33.createElement)("b", null, compact(value))
))
) : null
),
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)(
++ columns.map((column, at) => (0, import_react33.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 : ""
@@ -16574,103 +12333,103 @@ 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_react32.createElement)(
++ series.length > 1 || series[0]?.pick ? (0, import_react33.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", {
-+ series.map((entry) => (0, import_react32.createElement)(entry.pick ? "button" : "span", {
++ series.map((entry) => (0, import_react33.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_react32.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
++ }, (0, import_react33.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_react32.createElement)(
++ return (0, import_react33.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
-@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick
+@@ -9540,69 +13996,69 @@
title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }),
...onClick ? { type: "button", onClick } : {}
},
- glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react24.createElement)(
-+ glyph ? (0, import_react32.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
-+ (0, import_react32.createElement)(
++ glyph ? (0, import_react33.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
++ (0, import_react33.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-us-row-name" }, name),
-+ sub ? (0, import_react32.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
++ (0, import_react33.createElement)("span", { className: "bt-us-row-name" }, name),
++ sub ? (0, import_react33.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_react32.createElement)("span", { className: "bt-us-track" }, (0, import_react32.createElement)("span", { style: { width: `${width}%`, background: fill2 } }))
++ (0, import_react33.createElement)("span", { className: "bt-us-track" }, (0, import_react33.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_react32.createElement)("span", { className: "bt-us-row-num" }, (0, import_react32.createElement)("b", null, compact(sum.total)), (0, import_react32.createElement)("small", null, `${percent(sum.total, whole)}%`))
++ (0, import_react33.createElement)("span", { className: "bt-us-row-num" }, (0, import_react33.createElement)("b", null, compact(sum.total)), (0, import_react33.createElement)("small", null, `${percent(sum.total, whole)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("div", { className: "bt-us-card-head" }, (0, import_react32.createElement)("h3", null, title, count === void 0 ? null : (0, import_react32.createElement)("span", null, count))),
-+ (0, import_react32.createElement)("div", { className: "bt-us-rows" }, children)
++ (0, import_react33.createElement)("div", { className: "bt-us-card-head" }, (0, import_react33.createElement)("h3", null, title, count === void 0 ? null : (0, import_react33.createElement)("span", null, count))),
++ (0, import_react33.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_react32.createElement)(
++ return (0, import_react33.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("defs", null, (0, import_react32.createElement)(
++ face.stops ? (0, import_react33.createElement)("defs", null, (0, import_react33.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_react32.createElement)("stop", { key: offset, offset, stopColor: color }))
++ face.stops.map(([offset, color]) => (0, import_react33.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_react32.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
-+ arc > 0 ? (0, import_react32.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_react33.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
++ arc > 0 ? (0, import_react33.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_react32.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
++ (0, import_react33.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
),
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("h3", null, face.name),
-+ face.sub ? (0, import_react32.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
-+ (0, import_react32.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_react33.createElement)("h3", null, face.name),
++ face.sub ? (0, import_react33.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
++ (0, import_react33.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) }))
)
);
}
@@ -16678,77 +12437,77 @@ 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_react32.useRef)(null);
++ const box = (0, import_react33.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_react32.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
-+ const [anchor, setAnchor] = (0, import_react32.useState)(null);
-+ const [hover, setHover] = (0, import_react32.useState)(null);
-+ (0, import_react32.useEffect)(() => {
++ const [right, setRight] = (0, import_react33.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
++ const [anchor, setAnchor] = (0, import_react33.useState)(null);
++ const [hover, setHover] = (0, import_react33.useState)(null);
++ (0, import_react33.useEffect)(() => {
const onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
-@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
+@@ -9628,17 +14084,17 @@
const [from, to] = [anchor, key].sort();
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_react32.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []);
++ const weekdays = (0, import_react33.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_react32.createElement)("span", { key: `b${at}` }));
++ const cells = Array.from({ length: lead }, (_, at) => (0, import_react33.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_react32.createElement)("button", {
++ cells.push((0, import_react33.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
-@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
+@@ -9654,15 +14110,15 @@
"data-has": flag(active.has(key)),
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react24.createElement)("b", null, day)));
-+ }, (0, import_react32.createElement)("b", null, day)));
++ }, (0, import_react33.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_react32.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_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.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_react32.createElement)(ChevronRightIcon));
-+ return (0, import_react32.createElement)(
++ const nav = side === "left" ? (0, import_react33.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_react33.createElement)(ChevronLeftIcon)) : (0, import_react33.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_react33.createElement)(ChevronRightIcon));
++ return (0, import_react33.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_react32.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react32.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
-+ (0, import_react32.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react32.createElement)("span", { key: at }, name))),
-+ (0, import_react32.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
++ (0, import_react33.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react33.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
++ (0, import_react33.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react33.createElement)("span", { key: at }, name))),
++ (0, import_react33.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
);
};
const shortcuts = [
-@@ -9671,30 +9671,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
+@@ -9671,30 +14127,30 @@
["year", t("This year"), `${today.slice(0, 4)}-01-01`, today]
];
const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1;
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("button", {
++ shortcuts.map(([id, label, from, to]) => (0, import_react33.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
@@ -16757,110 +12516,110 @@ 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_react32.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
++ (0, import_react33.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_react32.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
-+ low === null ? null : (0, import_react32.createElement)("span", null, (0, import_react32.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
++ (0, import_react33.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
++ low === null ? null : (0, import_react33.createElement)("span", null, (0, import_react33.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_react32.useState)(null);
++ const [hover, setHover] = (0, import_react33.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);
-@@ -9710,13 +9710,13 @@ function Heat({ view, keep, paint, index }) {
+@@ -9710,13 +14166,13 @@
for (let week = 0; week < HEAT_WEEKS; week += 1) {
const monday = first + week * 7 * DAY_MS;
if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) {
- months.push((0, import_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
-+ months.push((0, import_react32.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
++ months.push((0, import_react33.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_react32.createElement)("span", {
++ cells.push((0, import_react33.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
-@@ -9724,7 +9724,7 @@ function Heat({ view, keep, paint, index }) {
+@@ -9724,7 +14180,7 @@
"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_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
-+ }, level > 0 ? (0, import_react32.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
++ }, level > 0 ? (0, import_react33.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
}
}
const tipSide = hover ? {
-@@ -9733,61 +9733,61 @@ function Heat({ view, keep, paint, index }) {
+@@ -9733,61 +14189,61 @@
} : null;
const active = [...daily.values()].filter((value) => value > 0).length;
const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active });
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("div", { className: "bt-us-card-head" }, (0, import_react32.createElement)("h3", null, t("Activity")), (0, import_react32.createElement)("span", { className: "bt-us-head-note" }, summary)),
-+ (0, import_react32.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-card-head" }, (0, import_react33.createElement)("h3", null, t("Activity")), (0, import_react33.createElement)("span", { className: "bt-us-head-note" }, summary)),
++ (0, import_react33.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
-+ hover ? (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
++ hover ? (0, import_react33.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_react32.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
-+ (0, import_react32.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
++ (0, import_react33.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
++ (0, import_react33.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_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react32.createElement)("i", { style: { background: soften(paint, level) } }))),
++ levels.map((level) => (0, import_react33.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react33.createElement)("i", { style: { background: soften(paint, level) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
-+ (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
++ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
-+ (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
++ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
++ (0, import_react33.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
)
)
);
@@ -16874,102 +12633,102 @@ 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_react32.useState)(null);
-+ const [error, setError] = (0, import_react32.useState)("");
-+ const [rangeId, setRangeId] = (0, import_react32.useState)("7d");
-+ const [span, setSpan] = (0, import_react32.useState)(null);
-+ const [picking, setPicking] = (0, import_react32.useState)(false);
-+ const [by, setBy] = (0, import_react32.useState)("bot");
-+ const tabs = (0, import_react32.useRef)(null);
++ const [view, setView] = (0, import_react33.useState)(null);
++ const [error, setError] = (0, import_react33.useState)("");
++ const [rangeId, setRangeId] = (0, import_react33.useState)("7d");
++ const [span, setSpan] = (0, import_react33.useState)(null);
++ const [picking, setPicking] = (0, import_react33.useState)(false);
++ const [by, setBy] = (0, import_react33.useState)("bot");
++ const tabs = (0, import_react33.useRef)(null);
const focus = focusOf(focusId);
- (0, import_react24.useEffect)(() => {
-+ (0, import_react32.useEffect)(() => {
++ (0, import_react33.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(() => {
-@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9797,7 +14253,7 @@
const value = await actions.usage();
await resized;
if (stopped) return;
- (0, import_react24.startTransition)(() => setView(value));
-+ (0, import_react32.startTransition)(() => setView(value));
++ (0, import_react33.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
-@@ -9814,7 +9814,7 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9814,7 +14270,7 @@
}, []);
const today = view?.now.slice(0, 10);
const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId);
- const shaped = (0, import_react24.useMemo)(() => {
-+ const shaped = (0, import_react32.useMemo)(() => {
++ const shaped = (0, import_react33.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
-@@ -9823,7 +9823,7 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9823,7 +14279,7 @@
const models2 = view.models.map((model, at) => {
const name = model.name || model.model || t("Unknown model");
const paint = modelPaint.get(at) ?? "var(--bt-us-rest)";
- 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_react32.createElement)(ModelGlyph, { name, paint, size }) };
++ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react33.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;
-@@ -9871,23 +9871,23 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9871,23 +14327,23 @@
});
return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 };
}, [view, rangeId, span, by, focusId, roster]);
- const root = (0, import_react24.useRef)(null);
- (0, import_react24.useEffect)(() => {
-+ const root = (0, import_react32.useRef)(null);
-+ (0, import_react32.useEffect)(() => {
++ const root = (0, import_react33.useRef)(null);
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react32.createElement)(Skeleton2);
++ return error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react33.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
-+ (0, import_react32.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
-+ (0, import_react32.createElement)("span", null, t("Token usage shows up here after a model is called."))
++ (0, import_react33.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
++ (0, import_react33.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
++ (0, import_react33.createElement)("span", null, t("Token usage shows up here after a model is called."))
)
);
}
-@@ -9912,7 +9912,7 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9912,7 +14368,7 @@
["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_react24.createElement)(Row2, {
-+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react32.createElement)(Row2, {
++ const ownerRow = ([at, sum], index, total = whole) => (0, import_react33.createElement)(Row2, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
-@@ -9924,7 +9924,7 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9924,7 +14380,7 @@
fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint),
onClick: () => pick2({ kind: "owner", key: view.owners[at].key })
});
- const modelRow = ([at, sum], index) => (0, import_react24.createElement)(Row2, {
-+ const modelRow = ([at, sum], index) => (0, import_react32.createElement)(Row2, {
++ const modelRow = ([at, sum], index) => (0, import_react33.createElement)(Row2, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
-@@ -9936,30 +9936,30 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9936,30 +14392,30 @@
fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint),
onClick: () => pick2({ kind: "model", key: view.models[at].key })
});
- const nothing = (0, import_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
-+ const nothing = (0, import_react32.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
++ const nothing = (0, import_react33.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
let body;
if (face) {
const kinds = ranked(part.kinds);
@@ -16978,88 +12737,88 @@ 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_react32.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
++ (0, import_react33.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_react32.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
-+ (0, import_react32.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
++ (0, import_react33.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
++ (0, import_react33.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_react32.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react32.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_react32.createElement)(
++ ownerAt >= 0 ? (0, import_react33.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react33.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_react33.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_react32.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_react33.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_react32.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
++ (0, import_react33.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
)
];
} else {
-@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) {
+@@ -9968,39 +14424,39 @@
const rest = owners.filter(([at]) => !isBot(at));
const usedModels = ranked(all.models);
body = [
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
-+ (0, import_react32.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
++ (0, import_react33.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
++ (0, import_react33.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
++ bots.length > 0 && rest.length > 0 ? (0, import_react33.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_react32.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
++ (0, import_react33.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_react32.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
++ (0, import_react33.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
)
];
}
- return (0, import_react24.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react24.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
-+ view.scanning ? (0, import_react32.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
-+ picking ? (0, import_react32.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
++ (0, import_react33.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
++ view.scanning ? (0, import_react33.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
++ picking ? (0, import_react33.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_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
++ error ? (0, import_react33.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
body
);
}
-@@ -10150,33 +10150,135 @@ button.bt-us-row:hover{background:var(--bt-hover)}
+@@ -10150,33 +14606,135 @@
@keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}}
@keyframes bt-us-fade{from{opacity:0}}
@keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}}
@@ -17072,7 +12831,7 @@ diff --git a/client.js b/client.js
// src/client/connectors.js
-var import_react25 = require("react");
-+var import_react33 = require("react");
++var import_react34 = require("react");
var TOKEN_URL = "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/settings/personal-access-tokens/new";
-var GitHubMark = () => (0, import_react25.createElement)(
+var COMPOSIO_KEY_URL = "https://platform.composio.dev/";
@@ -17106,11 +12865,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_react33.createElement)(
++var GitHubMark = () => (0, import_react34.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_react33.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_react34.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") };
@@ -17134,20 +12893,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_react33.createElement)(
++ return (0, import_react34.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_react33.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
++ label ? (0, import_react34.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ );
+}
+function AppPicker({ apps, onChange, search, statuses = {} }) {
-+ const [query, setQuery] = (0, import_react33.useState)("");
-+ const [found, setFound] = (0, import_react33.useState)(null);
-+ const [failed, setFailed] = (0, import_react33.useState)(null);
-+ const names = (0, import_react33.useRef)(new Map(APPS));
-+ const asked = (0, import_react33.useRef)(0);
-+ (0, import_react33.useEffect)(() => {
++ const [query, setQuery] = (0, import_react34.useState)("");
++ const [found, setFound] = (0, import_react34.useState)(null);
++ const [failed, setFailed] = (0, import_react34.useState)(null);
++ const names = (0, import_react34.useRef)(new Map(APPS));
++ const asked = (0, import_react34.useRef)(0);
++ (0, import_react34.useEffect)(() => {
+ const text = query.trim();
+ if (text === "" || search === void 0) {
+ setFound(null);
@@ -17174,13 +12933,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_react33.createElement)(
++ return (0, import_react34.createElement)(
+ "div",
+ { className: "bt-connector-apps" },
-+ apps.length > 0 ? (0, import_react33.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react33.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
-+ (0, import_react33.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_react33.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
-+ (0, import_react33.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react33.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react33.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
++ apps.length > 0 ? (0, import_react34.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react34.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
++ (0, import_react34.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_react34.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
++ (0, import_react34.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react34.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)))
@@ -17188,32 +12947,32 @@ 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_react33.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react33.createElement)(
++ return (0, import_react34.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react34.createElement)(
+ "li",
+ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
-+ (0, import_react33.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
-+ (0, import_react33.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
++ (0, import_react34.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
++ (0, import_react34.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
+ )));
+}
+function ConnectorRow({ connector, roster, actions }) {
-+ const [form, setForm] = (0, import_react33.useState)(false);
-+ const [bots, setBots] = (0, import_react33.useState)(false);
-+ const [token, setToken] = (0, import_react33.useState)("");
-+ const [apps, setApps] = (0, import_react33.useState)(() => connector.apps ?? []);
-+ const [picking, setPicking] = (0, import_react33.useState)(false);
-+ const [busy, setBusy] = (0, import_react33.useState)(false);
-+ const [error, setError] = (0, import_react33.useState)(null);
++ const [form, setForm] = (0, import_react34.useState)(false);
++ const [bots, setBots] = (0, import_react34.useState)(false);
++ const [token, setToken] = (0, import_react34.useState)("");
++ const [apps, setApps] = (0, import_react34.useState)(() => connector.apps ?? []);
++ const [picking, setPicking] = (0, import_react34.useState)(false);
++ const [busy, setBusy] = (0, import_react34.useState)(false);
++ const [error, setError] = (0, import_react34.useState)(null);
const run = (task, after) => {
setBusy(true);
setError(null);
-@@ -10184,72 +10184,128 @@ function ConnectorRow({ connector, actions }) {
+@@ -10184,72 +14742,128 @@
after?.();
}, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false));
};
+ const gateway = connector.id === "composio";
-+ const [statuses, setStatuses] = (0, import_react33.useState)({});
++ const [statuses, setStatuses] = (0, import_react34.useState)({});
+ const appsKey = (connector.apps ?? []).join(",");
-+ (0, import_react33.useEffect)(() => {
++ (0, import_react34.useEffect)(() => {
+ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
+ setStatuses({});
+ return void 0;
@@ -17228,7 +12987,7 @@ diff --git a/client.js b/client.js
+ live = false;
+ };
+ }, [gateway, connector.state, appsKey, picking]);
-+ const search = (0, import_react33.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
++ const search = (0, import_react34.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), () => {
@@ -17254,113 +13013,113 @@ 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_react33.useRef)(null);
++ const row = (0, import_react34.useRef)(null);
+ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`);
-+ return (0, import_react33.createElement)(
++ return (0, import_react34.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react25.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react33.createElement)(Mark) : null),
-+ (0, import_react33.createElement)(
++ (0, import_react34.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react34.createElement)(Mark) : null),
++ (0, import_react34.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_react33.createElement)("span", { className: "bt-connector-name" }, connector.name),
-+ (0, import_react33.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
++ (0, import_react34.createElement)("span", { className: "bt-connector-name" }, connector.name),
++ (0, import_react34.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
),
- (0, import_react25.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ connector.state === "error" ? (0, import_react34.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_react34.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_react33.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_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
++ on ? (0, import_react34.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_react34.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_react34.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_react33.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
-+ on ? (0, import_react33.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
-+ on && bots ? (0, import_react33.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
-+ !on && form ? (0, import_react33.createElement)(
++ on && gateway && !picking ? (0, import_react34.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
++ on ? (0, import_react34.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
++ on && bots ? (0, import_react34.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
++ !on && form ? (0, import_react34.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_react33.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react33.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
-+ gateway ? (0, import_react33.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
-+ (0, import_react33.createElement)(
++ (0, import_react34.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react34.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
++ gateway ? (0, import_react34.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
++ (0, import_react34.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_react33.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ (0, import_react34.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_react34.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_react33.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_react33.createElement)(
++ (0, import_react34.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_react34.createElement)(
+ "div",
+ { className: "bt-connector-form" },
-+ (0, import_react33.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
-+ (0, import_react33.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_react33.createElement)(
++ (0, import_react34.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
++ (0, import_react34.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_react34.createElement)(
+ "div",
+ { className: "bt-connector-form-row" },
-+ (0, import_react33.createElement)("span", { className: "bt-connector-link" }),
-+ (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ (0, import_react34.createElement)("span", { className: "bt-connector-link" }),
++ (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ setPicking(false);
+ setError(null);
+ } }, t("Cancel")),
-+ (0, import_react33.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_react33.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
++ (0, import_react34.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_react34.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_react33.createElement)(
++ return (0, import_react34.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_react33.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-set-row" }, (0, import_react33.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react33.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
++ (0, import_react34.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-set-row" }, (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react34.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_react34 = require("react");
-+var FeedbackIcon = () => (0, import_react34.createElement)(
++var import_react35 = require("react");
++var FeedbackIcon = () => (0, import_react35.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_react34.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_react34.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" })
++ (0, import_react35.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_react35.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" }));
-@@ -10260,7 +10260,7 @@ function download(name, text) {
+@@ -10260,7 +14874,7 @@
setTimeout(() => URL.revokeObjectURL(url), 1e3);
}
var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false);
@@ -17369,7 +13128,7 @@ diff --git a/client.js b/client.js
const date = new Date(time);
const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString();
const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" });
-@@ -10270,18 +10270,18 @@ function entrySummary(entry) {
+@@ -10270,18 +14884,18 @@
if (entry.source === "turn") return failureLabel(entry.code, entry.text);
return entry.text;
}
@@ -17380,148 +13139,148 @@ 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_react34.useState)("");
-+ const [report, setReport] = (0, import_react34.useState)(null);
-+ const [preview, setPreview] = (0, import_react34.useState)(false);
-+ const [note, setNote] = (0, import_react34.useState)("");
++ const [what, setWhat] = (0, import_react35.useState)("");
++ const [report, setReport] = (0, import_react35.useState)(null);
++ const [preview, setPreview] = (0, import_react35.useState)(false);
++ const [note, setNote] = (0, import_react35.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_react34.useEffect)(() => {
++ (0, import_react35.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
-@@ -10289,7 +10289,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
+@@ -10289,7 +14903,7 @@
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_react26.useState)(null);
-+ const [issueLink, setIssueLink] = (0, import_react34.useState)(null);
++ const [issueLink, setIssueLink] = (0, import_react35.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
-@@ -10317,59 +10317,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
+@@ -10317,59 +14931,59 @@
const total = (report?.total ?? 0) + browserErrors.length;
const shown = entries2.slice(0, 8);
return [
- (0, import_react26.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react26.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react26.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.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_react34.createElement)("span", null, t("What happened?")),
-+ (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
++ (0, import_react35.createElement)("span", null, t("What happened?")),
++ (0, import_react35.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_react34.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_react35.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_react34.createElement)(
++ (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
-+ (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
++ (0, import_react35.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_react34.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react34.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
-+ (0, import_react34.createElement)(
++ preview ? (0, import_react35.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react35.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
++ (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react26.createElement)(
-+ note ? (0, import_react34.createElement)(
++ note ? (0, import_react35.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_react34.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
++ issueLink ? [" ", (0, import_react35.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
) : null,
- (0, import_react26.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.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_react34.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_react34.createElement)(
++ shown.length === 0 ? (0, import_react35.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_react35.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_react34.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
++ (0, import_react35.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_react34.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
-+ (0, import_react34.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · "))
++ (0, import_react35.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
++ (0, import_react35.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_react34.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
++ entry.code ? (0, import_react35.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
)),
- (0, import_react26.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
-+ (0, import_react34.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
)
)
];
-@@ -10377,8 +10377,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
+@@ -10377,8 +14991,8 @@
var TOAST_MS = 6e3;
function ErrorToast({ useUi, actions }) {
const toast = useUi((state) => state.toast);
- const [leaving, setLeaving] = (0, import_react26.useState)(false);
- (0, import_react26.useEffect)(() => {
-+ const [leaving, setLeaving] = (0, import_react34.useState)(false);
-+ (0, import_react34.useEffect)(() => {
++ const [leaving, setLeaving] = (0, import_react35.useState)(false);
++ (0, import_react35.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
-@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) {
+@@ -10389,15 +15003,16 @@
};
}, [toast?.token]);
if (!toast) return null;
- return (0, import_react26.createElement)(
+ const info = toast.tone === "info";
-+ return (0, import_react34.createElement)(
++ return (0, import_react35.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_react34.createElement)("span", { className: "bt-toast-text" }, toast.text),
-+ info ? null : (0, import_react34.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
++ (0, import_react35.createElement)("span", { className: "bt-toast-text" }, toast.text),
++ info ? null : (0, import_react35.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_react34.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react34.createElement)(CloseIcon))
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react35.createElement)(CloseIcon))
);
}
var FEEDBACK_CSS = `
-@@ -10416,47 +10416,56 @@ var FEEDBACK_CSS = `
+@@ -10416,47 +15031,56 @@
.bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word}
.bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}
.bt-event-error{color:#e02135}
@@ -17542,7 +13301,7 @@ diff --git a/client.js b/client.js
// src/client/about.js
-var import_react27 = require("react");
-var CHEER_MS = 1800;
-+var import_react35 = require("react");
++var import_react36 = 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…");
@@ -17585,13 +13344,13 @@ 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_react35.useState)(false);
-+ const [stars, setStars] = (0, import_react35.useState)(null);
-+ (0, import_react35.useEffect)(() => {
++ const [checking, setChecking] = (0, import_react36.useState)(false);
++ const [stars, setStars] = (0, import_react36.useState)(null);
++ (0, import_react36.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
-@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) {
+@@ -10466,87 +15090,72 @@
live = false;
};
}, []);
@@ -17619,14 +13378,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_react35.createElement)(
++ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react36.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_react35.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_react35.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_react35.createElement)(
++ ) : (0, import_react36.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_react36.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_react36.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")
@@ -17634,22 +13393,22 @@ diff --git a/client.js b/client.js
const repo = update.repoUrl;
return [
- (0, import_react27.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)("span", { className: "bt-about-mark" }, (0, import_react35.createElement)(FlyBotMark, { size: 52 })),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("span", { className: "bt-about-mark" }, (0, import_react36.createElement)(FlyBotMark, { size: 52 })),
++ (0, import_react36.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_react35.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
-+ update.current ? (0, import_react35.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
++ (0, import_react36.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
++ update.current ? (0, import_react36.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
),
- repo ? (0, import_react27.createElement)(
-+ repo ? (0, import_react35.createElement)(
++ repo ? (0, import_react36.createElement)(
"a",
{
className: "bt-star",
@@ -17663,31 +13422,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_react35.createElement)("span", { className: "bt-star-main" }, (0, import_react35.createElement)("span", { className: "bt-star-icon" }, (0, import_react35.createElement)(StarLineIcon)), "Star"),
-+ stars === null ? null : (0, import_react35.createElement)("span", { className: "bt-star-count" }, starCount(stars))
++ (0, import_react36.createElement)("span", { className: "bt-star-main" }, (0, import_react36.createElement)("span", { className: "bt-star-icon" }, (0, import_react36.createElement)(StarLineIcon)), "Star"),
++ stars === null ? null : (0, import_react36.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_react35.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_react35.createElement)(
++ repo ? (0, import_react36.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_react36.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_react35.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react35.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
-+ dsh?.current ? (0, import_react35.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
++ (0, import_react36.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react36.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
++ dsh?.current ? (0, import_react36.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
),
- (0, import_react27.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react35.createElement)("a", linkProps(repo), (0, import_react35.createElement)(GitHubMark), t("Open"))) : null,
-+ update.notesUrl ? (0, import_react35.createElement)(Row3, { label: t("Release notes") }, (0, import_react35.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
-+ (0, import_react35.createElement)(Row3, { label: t("Report a problem") }, (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
++ repo ? (0, import_react36.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react36.createElement)("a", linkProps(repo), (0, import_react36.createElement)(GitHubMark), t("Open"))) : null,
++ update.notesUrl ? (0, import_react36.createElement)(Row3, { label: t("Release notes") }, (0, import_react36.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
++ (0, import_react36.createElement)(Row3, { label: t("Report a problem") }, (0, import_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
)
];
}
@@ -17702,29 +13461,29 @@ diff --git a/client.js b/client.js
.bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.bt-about-name{font-size:16px;line-height:22px;font-weight:600}
.bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums}
-@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s
+@@ -10569,54 +15178,59 @@
`;
// src/client/settings.js
-var import_react28 = require("react");
-+var import_react36 = require("react");
++var import_react37 = 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_react36.createElement)("section", { className: "bt-set-section" }, (0, import_react36.createElement)("h3", null, title), (0, import_react36.createElement)("div", { className: "bt-set-card" }, children));
++ return (0, import_react37.createElement)("section", { className: "bt-set-section" }, (0, import_react37.createElement)("h3", null, title), (0, import_react37.createElement)("div", { className: "bt-set-card" }, children));
}
function SetRow({ label, hint, children }) {
- return (0, import_react28.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-set-copy" }, (0, import_react36.createElement)("span", null, label), hint ? (0, import_react36.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ children ? (0, import_react36.createElement)("div", { className: "bt-set-control" }, children) : null
++ (0, import_react37.createElement)("div", { className: "bt-set-copy" }, (0, import_react37.createElement)("span", null, label), hint ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, hint) : null),
++ children ? (0, import_react37.createElement)("div", { className: "bt-set-control" }, children) : null
);
}
+var NOTIFY_KINDS = [
@@ -17739,9 +13498,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_react36.useState)(prefs.accent ?? "#4d6bfe");
-+ const timer = (0, import_react36.useRef)(0);
-+ (0, import_react36.useEffect)(() => () => clearTimeout(timer.current), []);
++ const [custom, setCustom] = (0, import_react37.useState)(prefs.accent ?? "#4d6bfe");
++ const timer = (0, import_react37.useRef)(0);
++ (0, import_react37.useEffect)(() => () => clearTimeout(timer.current), []);
const pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
@@ -17749,53 +13508,53 @@ diff --git a/client.js b/client.js
};
return [
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("option", { key: id, value: id }, t(theme.label)))
++ Object.entries(themes).map(([id, theme]) => (0, import_react37.createElement)("option", { key: id, value: id }, t(theme.label)))
)
),
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.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_react36.createElement)("button", {
++ (0, import_react37.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_react37.createElement)("button", {
key: ink,
type: "button",
className: "bt-swatch",
-@@ -10626,75 +10626,87 @@ function GeneralPage({ roster, actions, run }) {
+@@ -10626,75 +15240,87 @@
style: { background: INKS[ink][0] },
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
++ (0, import_react37.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_react36.createElement)(
++ (0, import_react37.createElement)(
SetSection,
- { key: "mode", title: t("Mode") },
- (0, import_react28.createElement)(
@@ -17804,14 +13563,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_react36.createElement)(SwitchRow, {
++ (0, import_react37.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_react36.createElement)(SwitchRow, {
++ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react37.createElement)(SwitchRow, {
+ key: kind,
+ label: t(label),
+ hint: hint ? t(hint) : void 0,
@@ -17829,40 +13588,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_react36.createElement)(
++ return (0, import_react37.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("select", {
++ (0, import_react37.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_react36.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react36.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
++ }, first ? null : (0, import_react37.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react37.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
),
- others.map((bot) => (0, import_react28.createElement)(
-+ others.map((bot) => (0, import_react36.createElement)(
++ others.map((bot) => (0, import_react37.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_react36.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_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
++ (0, import_react37.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_react37.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_react36.createElement)(
++ candidates.length > 0 ? (0, import_react37.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_react36.createElement)("select", {
++ (0, import_react37.createElement)("select", {
className: "bt-select",
"aria-label": t("Add a Main Bot"),
value: "",
@@ -17870,7 +13629,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_react36.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react36.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
++ }, (0, import_react37.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react37.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
) : null
);
}
@@ -17878,47 +13637,47 @@ 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_react36.createElement)(
++ return (0, import_react37.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react36.createElement)("button", {
++ (0, import_react37.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react37.createElement)("button", {
key: id,
type: "button",
"aria-pressed": motion === id,
-@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) {
+@@ -10706,69 +15332,69 @@
);
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react36.useState)(false);
++ const [creating, setCreating] = (0, import_react37.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_react36.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
-+ (0, import_react36.createElement)(MotionSection, { key: "motion", roster, actions, run }),
-+ creating ? (0, import_react36.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react36.createElement)(
++ (0, import_react37.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
++ (0, import_react37.createElement)(MotionSection, { key: "motion", roster, actions, run }),
++ creating ? (0, import_react37.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react37.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_react36.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react36.createElement)(PlusIcon), t("New Bot"))
++ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react37.createElement)(PlusIcon), t("New Bot"))
),
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(
++ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react37.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
@@ -17926,42 +13685,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_react36.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
-+ (0, import_react36.createElement)("span", { className: "bt-row-title" }, bot.name),
-+ bot.role ? (0, import_react36.createElement)("span", { className: "bt-tag" }, bot.role) : null,
-+ isMainOf(roster, bot.id) ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
-+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
++ (0, import_react37.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
++ (0, import_react37.createElement)("span", { className: "bt-row-title" }, bot.name),
++ bot.role ? (0, import_react37.createElement)("span", { className: "bt-tag" }, bot.role) : null,
++ isMainOf(roster, bot.id) ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
++ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
++ (0, import_react37.createElement)("span", { className: "bt-chevron" }, (0, import_react37.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react36.useState)(false);
++ const [creating, setCreating] = (0, import_react37.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_react36.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react36.createElement)(
++ creating ? (0, import_react37.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react37.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_react36.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_react36.createElement)(PlusIcon), t("Create group chat"))
++ (0, import_react37.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_react37.createElement)(PlusIcon), t("Create group chat"))
),
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.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_react37.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_react36.createElement)(
++ return (0, import_react37.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
@@ -17969,14 +13728,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_react36.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-+ (0, import_react36.createElement)("span", { className: "bt-row-title" }, room2.name),
-+ (0, import_react36.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
-+ admin ? (0, import_react36.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react36.createElement)(AdminStar), admin.name) : null,
-+ room2.hidden ? (0, import_react36.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
++ (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
++ (0, import_react37.createElement)("span", { className: "bt-row-title" }, room2.name),
++ (0, import_react37.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
++ admin ? (0, import_react37.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react37.createElement)(AdminStar), admin.name) : null,
++ room2.hidden ? (0, import_react37.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
++ (0, import_react37.createElement)("span", { className: "bt-chevron" }, (0, import_react37.createElement)(ChevronRightIcon))
);
})
)
@@ -17985,12 +13744,12 @@ 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_react36.useState)("");
-+ const close = (0, import_react36.useCallback)(() => actions.closeSettings(), []);
++ const [error, setError] = (0, import_react37.useState)("");
++ const close = (0, import_react37.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
-@@ -10779,104 +10779,112 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
+@@ -10779,104 +15405,112 @@
});
const bot = page === "bots" && id ? roster.byId[id] : void 0;
const room2 = page === "groups" && id ? roster.roomsById[id] : void 0;
@@ -18007,34 +13766,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_react36.useRef)({ key: pageKey, depth, move: "up" });
++ const last = (0, import_react37.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_react36.useState)(shown === "usage");
++ const [usageOpened, setUsageOpened] = (0, import_react37.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_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
-+ return (0, import_react36.createElement)(
++ const errorLine = error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react37.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_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ pages.map(([key, text]) => (0, import_react36.createElement)("button", {
++ (0, import_react37.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
++ pages.map(([key, text]) => (0, import_react37.createElement)("button", {
key,
type: "button",
className: "bt-settings-item",
@@ -18042,10 +13801,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_react36.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))),
++ }, (0, import_react37.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_react36.createElement)("button", {
++ (0, import_react37.createElement)("button", {
type: "button",
className: "bt-settings-item bt-settings-shell",
- title: t("Models, providers, and the rest of DeepSeek Harness"),
@@ -18056,23 +13815,23 @@ diff --git a/client.js b/client.js
openHarnessSettings();
}
- }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings"))
-+ }, (0, import_react36.createElement)(SlidersIcon), t("Harness settings"), (0, import_react36.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
++ }, (0, import_react37.createElement)(SlidersIcon), t("Harness settings"), (0, import_react37.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react28.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react36.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react36.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
++ back ? (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react37.createElement)(ChevronLeftIcon)) : null,
++ (0, import_react37.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_react36.createElement)(
++ shown === "usage" ? null : (0, import_react37.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
@@ -18087,41 +13846,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_react36.createElement)(GeneralPage, { roster, actions, run }) : null,
-+ bot ? (0, import_react36.createElement)("div", { className: "bt-settings-form" }, (0, import_react36.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
-+ shown === "bots" && !bot ? (0, import_react36.createElement)(BotsPage, { roster, actions, run }) : null,
-+ room2 ? (0, import_react36.createElement)("div", { className: "bt-settings-form" }, (0, import_react36.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
-+ shown === "groups" && !room2 ? (0, import_react36.createElement)(GroupsPage, { roster, actions, run }) : null,
-+ shown === "connectors" ? (0, import_react36.createElement)(ConnectorsPage, { roster, actions }) : null,
-+ pluginPage ? (0, import_react36.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
-+ shown === "capabilities" ? (0, import_react36.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
-+ shown === "schedules" ? (0, import_react36.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
-+ shown === "secrets" ? (0, import_react36.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
-+ shown === "feedback" ? (0, import_react36.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
-+ shown === "about" ? (0, import_react36.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
++ shown === "general" ? (0, import_react37.createElement)(GeneralPage, { roster, actions, run }) : null,
++ bot ? (0, import_react37.createElement)("div", { className: "bt-settings-form" }, (0, import_react37.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
++ shown === "bots" && !bot ? (0, import_react37.createElement)(BotsPage, { roster, actions, run }) : null,
++ room2 ? (0, import_react37.createElement)("div", { className: "bt-settings-form" }, (0, import_react37.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
++ shown === "groups" && !room2 ? (0, import_react37.createElement)(GroupsPage, { roster, actions, run }) : null,
++ shown === "connectors" ? (0, import_react37.createElement)(ConnectorsPage, { roster, actions }) : null,
++ pluginPage ? (0, import_react37.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
++ shown === "capabilities" ? (0, import_react37.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
++ shown === "schedules" ? (0, import_react37.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
++ shown === "secrets" ? (0, import_react37.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
++ shown === "feedback" ? (0, import_react37.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
++ shown === "about" ? (0, import_react37.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
),
- usageOpened ? (0, import_react28.createElement)(
-+ usageOpened ? (0, import_react36.createElement)(
++ usageOpened ? (0, import_react37.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_react36.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
++ (0, import_react37.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_react36.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react36.createElement)(CloseIcon))
++ (0, import_react37.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react37.createElement)(CloseIcon))
)
);
}
// src/client/context-menu.js
-var import_react29 = require("react");
-+var import_react37 = require("react");
++var import_react38 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react29.useEffect)(() => {
-+ (0, import_react37.useEffect)(() => {
++ (0, import_react38.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -18132,19 +13891,19 @@ 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_react37.useRef)(null);
-+ const [place, setPlace] = (0, import_react37.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react37.useLayoutEffect)(() => {
++ const ref = (0, import_react38.useRef)(null);
++ const [place, setPlace] = (0, import_react38.useState)({ left: menu.x, top: menu.y });
++ (0, import_react38.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_react37.createElement)(AgentMenu, { menu, roster, actions, leaving });
++ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react38.createElement)(AgentMenu, { menu, roster, actions, leaving });
const bot = roster.byId[menu.id];
const room2 = roster.roomsById[menu.id];
const entry = bot ?? room2;
-@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
+@@ -10884,13 +15518,22 @@
const isMainBot = isMainOf(roster, menu.id);
const isTile = roster.mainBotId === menu.id;
const mainCount = (roster.mainBotIds ?? []).length;
@@ -18169,7 +13928,7 @@ diff --git a/client.js b/client.js
bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null,
room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null,
room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null,
-@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
+@@ -10901,20 +15544,22 @@
bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null,
bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null
],
@@ -18183,27 +13942,27 @@ 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_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)(
-+ import_react37.Fragment,
++ shown.map((group, index) => (0, import_react38.createElement)(
++ import_react38.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_react37.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react37.createElement)("button", {
++ index > 0 ? (0, import_react38.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, danger]) => (0, import_react38.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
-@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
+@@ -10923,14 +15568,14 @@
actions.closeMenu();
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react37.createElement)(Icon), label))
++ }, (0, import_react38.createElement)(Icon), label))
))
);
}
@@ -18211,38 +13970,38 @@ 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_react37.useRef)(null);
-+ const [place, setPlace] = (0, import_react37.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react37.useLayoutEffect)(() => {
++ const ref = (0, import_react38.useRef)(null);
++ const [place, setPlace] = (0, import_react38.useState)({ left: menu.x, top: menu.y });
++ (0, import_react38.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)) });
-@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
+@@ -10952,14 +15597,14 @@
[t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
]
];
- return (0, import_react29.createElement)(
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)(
-+ import_react37.Fragment,
++ groups.map((group, index) => (0, import_react38.createElement)(
++ import_react38.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_react37.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react37.createElement)("button", {
++ index > 0 ? (0, import_react38.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, danger]) => (0, import_react38.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
-@@ -10968,303 +10968,93 @@ function AgentMenu({ menu, roster, actions, leaving }) {
+@@ -10968,16 +15613,31 @@
actions.closeMenu();
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react37.createElement)(Icon), label))
++ }, (0, import_react38.createElement)(Icon), label))
))
);
}
@@ -18270,63 +14029,10 @@ diff --git a/client.js b/client.js
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 {
-- }
--}
--
+@@ -11038,233 +15698,8 @@
+ }
+ }
+
-// src/client/member-status.js
-var import_react30 = require("react");
-var NO_WORK = { bots: {}, rooms: {}, approvals: [] };
@@ -18509,65 +14215,13 @@ 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
- )
- );
-+};
-+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}
@@ -18603,145 +14257,145 @@ 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_react38 = require("react");
++var import_react39 = 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 "";
-@@ -11289,10 +11289,10 @@ var EMPTY_TABS = { ids: [], active: null };
+@@ -11289,10 +15724,10 @@
var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" };
var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]);
function PermissionRow({ bot, roster, actions, run }) {
- const [pending, setPending] = (0, import_react31.useState)(null);
-+ const [pending, setPending] = (0, import_react38.useState)(null);
++ const [pending, setPending] = (0, import_react39.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react31.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
-@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) {
+@@ -11302,14 +15737,14 @@
setPending(value);
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react31.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react31.createElement)(
-+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
++ (0, import_react39.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react31.createElement)("button", {
-+ catalog.options.map((option) => (0, import_react38.createElement)("button", {
++ catalog.options.map((option) => (0, import_react39.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
-@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) {
+@@ -11323,42 +15758,42 @@
function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
const pages = useBrowserPages((map) => map);
- (0, import_react31.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.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_react38.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
++ return (0, import_react39.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_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
-+ (0, import_react38.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
-+ (0, import_react38.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_react38.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
++ (0, import_react39.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
++ (0, import_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
++ (0, import_react39.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_react39.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
);
}
- return (0, import_react31.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)(
++ (0, import_react39.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_react38.createElement)(
++ page.excerpt ? (0, import_react39.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_react38.createElement)("span", { className: "bt-bcard-ptitle" }, label),
-+ (0, import_react38.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
-+ ) : (0, import_react38.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react38.createElement)(GlobeIcon), (0, import_react38.createElement)("span", null, label))
++ (0, import_react39.createElement)("span", { className: "bt-bcard-ptitle" }, label),
++ (0, import_react39.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
++ ) : (0, import_react39.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react39.createElement)(GlobeIcon), (0, import_react39.createElement)("span", null, label))
),
- (0, import_react31.createElement)("button", {
-+ (0, import_react38.createElement)("button", {
++ (0, import_react39.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
-@@ -11367,87 +11367,195 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
+@@ -11367,87 +15802,195 @@
void actions.browserCloseTab(bot.id, tabId);
}
}, "×"),
- (0, import_react31.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
-+ (0, import_react38.createElement)("span", { className: "bt-bcard-title" }, label),
-+ page.url ? (0, import_react38.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
++ (0, import_react39.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
++ (0, import_react39.createElement)("span", { className: "bt-bcard-title" }, label),
++ page.url ? (0, import_react39.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_react38.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react38.createElement)(PlusIcon), t("New tab"))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react39.createElement)(PlusIcon), t("New tab"))
);
}
function About({ text, title = "About" }) {
- const [open, setOpen] = (0, import_react31.useState)(false);
-+ const [open, setOpen] = (0, import_react38.useState)(false);
++ const [open, setOpen] = (0, import_react39.useState)(false);
const long = text.split("\n").length > 5 || text.length > 240;
- const body = (0, import_react31.useRef)(null);
-+ const body = (0, import_react38.useRef)(null);
++ const body = (0, import_react39.useRef)(null);
useMorph(body, open);
- return (0, import_react31.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
+ "div",
+ null,
-+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t(title)),
-+ (0, import_react38.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
-+ long ? (0, import_react38.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t(title)),
++ (0, import_react39.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
++ long ? (0, import_react39.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_react38.useState)(room2.pendingApprovals ?? []);
-+ const [busy, setBusy] = (0, import_react38.useState)("");
++ const [items, setItems] = (0, import_react39.useState)(room2.pendingApprovals ?? []);
++ const [busy, setBusy] = (0, import_react39.useState)("");
+ const face = room2.origin === "face-to-face";
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
+ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
+ if (haveItems) setItems(room2.pendingApprovals);
+ if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -18767,31 +14421,31 @@ diff --git a/client.js b/client.js
+ setBusy("");
+ }
+ };
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
+ "div",
+ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
-+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react38.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
-+ items.map((item) => (0, import_react38.createElement)(
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react39.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
++ items.map((item) => (0, import_react39.createElement)(
+ "div",
+ { key: item.id, className: "bt-set-row" },
-+ (0, import_react38.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
-+ (0, import_react38.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
-+ (0, import_react38.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
++ (0, import_react39.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
++ (0, import_react39.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_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
-+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
-+ (0, import_react38.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react38.createElement)(FacePersonRow, {
++ (0, import_react39.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
++ (0, import_react39.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react39.createElement)(FacePersonRow, {
+ key: person.key,
+ person,
+ roster,
@@ -18817,31 +14471,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_react38.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
-+ invited && room2.sharedFrom && !face ? (0, import_react38.createElement)(
++ (0, import_react39.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
++ invited && room2.sharedFrom && !face ? (0, import_react39.createElement)(
+ "div",
+ { key: "from", className: "bt-shared-from" },
-+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, title),
-+ (0, import_react38.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_react38.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
-+ (0, import_react38.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
-+ ) : face ? null : room2.notice ? (0, import_react38.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react38.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
-+ face ? null : (0, import_react38.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
-+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react38.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react38.createElement)(
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, title),
++ (0, import_react39.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_react39.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
++ ) : face ? null : room2.notice ? (0, import_react39.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react39.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
++ face ? null : (0, import_react39.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
++ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react39.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
-+ (0, import_react38.createElement)("div", { className: "bt-members", role: "list" }, [
-+ ...humans.map((person) => (0, import_react38.createElement)(
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
++ (0, import_react39.createElement)("div", { className: "bt-members", role: "list" }, [
++ ...humans.map((person) => (0, import_react39.createElement)(
"div",
- { key: member.id, role: "listitem" },
- (0, import_react31.createElement)(
+ { key: person.id, role: "listitem" },
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react31.createElement)(
@@ -18849,70 +14503,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_react38.createElement)("span", { className: "bt-member-name" }, (0, import_react38.createElement)("span", null, person.displayName || person.id)),
-+ (0, import_react38.createElement)("span", { className: "bt-chip" }, t("Human")),
-+ person.role === "admin" ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ (0, import_react39.createElement)("span", { className: "bt-member-name" }, (0, import_react39.createElement)("span", null, person.displayName || person.id)),
++ (0, import_react39.createElement)("span", { className: "bt-chip" }, t("Human")),
++ person.role === "admin" ? (0, import_react39.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ )
+ )),
-+ ...remotes.map((person) => (0, import_react38.createElement)(
++ ...remotes.map((person) => (0, import_react39.createElement)(
+ "div",
+ { key: person.handle || person.id, role: "listitem" },
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ "span",
+ { className: "bt-member-name" },
-+ (0, import_react38.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react38.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
++ (0, import_react39.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
++ (0, import_react39.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_react38.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
++ face ? null : (0, import_react39.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ )
+ )),
+ ...members.map((member) => {
+ const entry = entries2.find((item) => item.id === member.id);
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
+ "div",
+ { key: member.id, role: "listitem" },
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ "button",
+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
-+ (0, import_react38.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
-+ (0, import_react38.createElement)("span", null, member.name)
++ (0, import_react39.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
++ (0, import_react39.createElement)("span", null, member.name)
+ ),
-+ room2.admin === member.id ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ room2.admin === member.id ? (0, import_react39.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ ),
-+ entry ? (0, import_react38.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
++ entry ? (0, import_react39.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ );
+ })
+ ])
),
- (0, import_react31.createElement)(
-+ face ? null : (0, import_react38.createElement)(
++ face ? null : (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
-+ (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
++ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
),
- usage ? (0, import_react31.createElement)(
-+ face || !usage ? null : (0, import_react38.createElement)(
++ face || !usage ? null : (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, room2.name), (0, import_react38.createElement)("span", null, usage))
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Usage")),
++ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, room2.name), (0, import_react39.createElement)("span", null, usage))
+ )
];
}
@@ -18921,181 +14575,181 @@ 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_react38.useState)("details");
++ const [tab, setTab] = (0, import_react39.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_react38.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
-+ const [editing, setEditing] = (0, import_react38.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
-+ const [draftName, setDraftName] = (0, import_react38.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
-+ const [copied, setCopied] = (0, import_react38.useState)(false);
-+ const [error, setError] = (0, import_react38.useState)("");
++ const [view, setView] = (0, import_react39.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
++ const [editing, setEditing] = (0, import_react39.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
++ const [draftName, setDraftName] = (0, import_react39.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
++ const [copied, setCopied] = (0, import_react39.useState)(false);
++ const [error, setError] = (0, import_react39.useState)("");
const glide = useGlide(tab);
- const last = (0, import_react31.useRef)({ view, tab, move: void 0 });
-+ const last = (0, import_react38.useRef)({ view, tab, move: void 0 });
++ const last = (0, import_react39.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;
-@@ -11472,36 +11472,43 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
+@@ -11472,36 +16015,43 @@
setTimeout(() => setCopied(false), 1600);
});
};
- 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_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ const errorLine = error ? (0, import_react39.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_react38.createElement)(
++ (0, import_react39.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_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react38.createElement)(ChevronLeftIcon)),
-+ (0, import_react38.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
-+ hasStrip ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react38.createElement)(CloseIcon))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react39.createElement)(ChevronLeftIcon)),
++ (0, import_react39.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
++ hasStrip ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react39.createElement)(CloseIcon))
),
- (0, import_react31.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react38.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
++ bot ? (0, import_react39.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react39.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
)
];
}
- const top = (0, import_react31.createElement)(
-+ const top = (0, import_react38.createElement)(
++ const top = (0, import_react39.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_react38.createElement)("button", {
++ bot || room2 ? (0, import_react39.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_react38.createElement)(GearIcon)) : null,
-+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react38.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_react38.createElement)(GlobeIcon)) : null,
-+ bot || room2 ? (0, import_react38.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_react38.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react38.createElement)(ShareIcon)) : null,
-+ hasStrip ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react38.createElement)(CloseIcon))
++ }, (0, import_react39.createElement)(GearIcon)) : null,
++ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react39.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_react39.createElement)(GlobeIcon)) : null,
++ bot || room2 ? (0, import_react39.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_react39.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react39.createElement)(ShareIcon)) : null,
++ hasStrip ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react39.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_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
-+ (0, import_react38.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react38.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
++ (0, import_react39.createElement)(import_react39.Fragment, { key: "top" }, top),
++ (0, import_react39.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react39.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
];
}
const current = bot?.name ?? room2.name;
-@@ -11515,12 +11515,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
+@@ -11515,12 +16065,12 @@
const subtitle = bot ? bot.role : roomSubtitle(room2, roster);
const tabs = bot ? ["details", "browser"] : ["details"];
return [
- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top),
- (0, import_react31.createElement)(
-+ (0, import_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(import_react39.Fragment, { key: "top" }, top),
++ (0, import_react39.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_react38.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react38.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ bot?.remote ? (0, import_react38.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react38.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react38.createElement)("input", {
++ bot ? (0, import_react39.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react39.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
++ bot?.remote ? (0, import_react39.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react39.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react39.createElement)("input", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
-@@ -11535,87 +11535,86 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
+@@ -11535,87 +16085,86 @@
setEditing(false);
}
}
- }) : (0, import_react31.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
-+ }) : (0, import_react38.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
++ }) : (0, import_react39.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_react38.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react38.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
++ subtitle ? (0, import_react39.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react39.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
),
- (0, import_react31.createElement)(
-+ bot?.remote ? null : (0, import_react38.createElement)(
++ bot?.remote ? null : (0, import_react39.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_react38.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ tabs.map((key) => (0, import_react38.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
++ (0, import_react39.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ tabs.map((key) => (0, import_react39.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_react38.createElement)(
++ (0, import_react39.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_react38.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
++ room2 && tab === "details" ? (0, import_react39.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ bot && bot.remote && tab === "details" ? [
-+ bot.remote.handle ? (0, import_react38.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
-+ (0, import_react38.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
++ bot.remote.handle ? (0, import_react39.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
++ (0, import_react39.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
+ ] : null,
+ bot && !bot.remote && tab === "details" ? [
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Model")),
-+ (0, import_react38.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
-+ modelOf(bot, roster) ? null : (0, import_react38.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Model")),
++ (0, import_react39.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
++ modelOf(bot, roster) ? null : (0, import_react39.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_react38.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
++ (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
-+ (0, import_react38.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
++ (0, import_react39.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_react38.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react38.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
-+ roster.memory === false ? null : (0, import_react38.createElement)(MemoryRow, { key: "memory", bot, actions }),
-+ usage ? (0, import_react38.createElement)(
++ bot.instructions ? (0, import_react39.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react39.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
++ roster.memory === false ? null : (0, import_react39.createElement)(MemoryRow, { key: "memory", bot, actions }),
++ usage ? (0, import_react39.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_react38.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react38.createElement)("div", { className: "bt-kv" }, (0, import_react38.createElement)("span", null, bot.name), (0, import_react38.createElement)("span", null, usage))
++ (0, import_react39.createElement)("div", { className: "bt-section-title" }, t("Usage")),
++ (0, import_react39.createElement)("div", { className: "bt-kv" }, (0, import_react39.createElement)("span", null, bot.name), (0, import_react39.createElement)("span", null, usage))
) : null,
- (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions })
-+ (0, import_react38.createElement)(ScheduleSummary, { key: "routines", bot, actions })
++ (0, import_react39.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_react38.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
++ bot && !bot.remote && tab === "browser" ? (0, import_react39.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
)
];
}
// src/client/browser-pane.js
-var import_react32 = require("react");
-+var import_react39 = require("react");
++var import_react40 = require("react");
var import_react_dom8 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
@@ -19112,11 +14766,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_react39.createElement)(
++ return (0, import_react40.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_react39.createElement)("path", { key: index, d }))
++ paths.map((d, index) => (0, import_react40.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"]);
@@ -19139,13 +14793,13 @@ 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_react39.useState)(null);
-+ if (!src || failed === src) return (0, import_react39.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react39.createElement)(GlobeIcon));
-+ return (0, import_react39.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
++ const [failed, setFailed] = (0, import_react40.useState)(null);
++ if (!src || failed === src) return (0, import_react40.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react40.createElement)(GlobeIcon));
++ return (0, import_react40.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
}
var savedWidth = () => {
try {
-@@ -11625,8 +11625,8 @@ var savedWidth = () => {
+@@ -11625,8 +16174,8 @@
return null;
}
};
@@ -19155,7 +14809,7 @@ diff --git a/client.js b/client.js
var room = { rule: null, selector: "" };
var roomRule = () => {
const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected;
-@@ -11674,11 +11674,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref
+@@ -11674,11 +16223,11 @@
var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab");
function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) {
const pages = useBrowserPages((map) => map);
@@ -19164,82 +14818,82 @@ 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_react39.useRef)(null);
-+ const ind = (0, import_react39.useRef)(null);
-+ const spot = (0, import_react39.useRef)(null);
-+ const under = (0, import_react39.useRef)(null);
-+ (0, import_react39.useLayoutEffect)(() => {
++ const wrap = (0, import_react40.useRef)(null);
++ const ind = (0, import_react40.useRef)(null);
++ const spot = (0, import_react40.useRef)(null);
++ const under = (0, import_react40.useRef)(null);
++ (0, import_react40.useLayoutEffect)(() => {
const box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
-@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
+@@ -11707,7 +16256,7 @@
trail.cancel();
};
});
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
-@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
+@@ -11721,13 +16270,13 @@
observer.observe(box);
return () => observer.disconnect();
}, []);
- return (0, import_react32.createElement)(
-+ return (0, import_react39.createElement)(
++ return (0, import_react40.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react32.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react32.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)(
"button",
{
type: "button",
-@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
+@@ -11737,13 +16286,13 @@
title: bot.name,
onClick: () => onView("details")
},
- (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_react39.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
-+ view === "details" ? (0, import_react39.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
++ (0, import_react40.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
++ view === "details" ? (0, import_react40.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_react39.createElement)(
++ return (0, import_react40.createElement)(
"button",
{
key: tabId,
-@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
+@@ -11764,9 +16313,9 @@
}
}
},
- (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_react39.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
-+ (0, import_react39.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
-+ (0, import_react39.createElement)("span", {
++ (0, import_react40.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
++ (0, import_react40.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
++ (0, import_react40.createElement)("span", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
-@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
+@@ -11778,20 +16327,20 @@
}, "×")
);
}),
- (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
-+ (0, import_react39.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
++ (0, import_react40.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_react39.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react39.createElement)(PlusTabIcon)),
-+ (0, import_react39.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_react39.createElement)(CloseIcon2))
++ (0, import_react40.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react40.createElement)(PlusTabIcon)),
++ (0, import_react40.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_react40.createElement)(CloseIcon2))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
@@ -19247,42 +14901,42 @@ 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_react39.useRef)(null);
-+ const input = (0, import_react39.useRef)(null);
-+ const [draft, setDraft] = (0, import_react39.useState)(null);
++ const frame = (0, import_react40.useRef)(null);
++ const input = (0, import_react40.useRef)(null);
++ const [draft, setDraft] = (0, import_react40.useState)(null);
const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react39.useLayoutEffect)(() => {
++ (0, import_react40.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
-@@ -11817,7 +11817,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
+@@ -11817,7 +16366,7 @@
window.removeEventListener("resize", schedule);
};
}, [tabId, showGuest, leaving]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
-@@ -11833,27 +11833,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
+@@ -11833,27 +16382,27 @@
};
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react32.createElement)(
-+ return (0, import_react39.createElement)(
++ return (0, import_react40.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react32.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.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_react39.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_react39.createElement)(BackIcon)),
-+ (0, import_react39.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_react39.createElement)(ForwardIcon)),
-+ (0, import_react39.createElement)("button", {
++ (0, import_react40.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_react40.createElement)(BackIcon)),
++ (0, import_react40.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_react40.createElement)(ForwardIcon)),
++ (0, import_react40.createElement)("button", {
type: "button",
className: "bt-icon-btn",
disabled: !page.url,
@@ -19291,54 +14945,54 @@ 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_react39.createElement)(page.loading ? StopIcon : ReloadIcon)),
-+ (0, import_react39.createElement)(
++ }, (0, import_react40.createElement)(page.loading ? StopIcon : ReloadIcon)),
++ (0, import_react40.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_react39.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react39.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
-+ (0, import_react39.createElement)("input", {
++ (0, import_react40.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react40.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
++ (0, import_react40.createElement)("input", {
ref: input,
type: "text",
spellCheck: false,
-@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
+@@ -11877,25 +16426,25 @@
}
})
),
- page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
-+ page.loading ? (0, import_react39.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
++ page.loading ? (0, import_react40.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
),
- (0, import_react32.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react32.createElement)(
-+ page.error ? (0, import_react39.createElement)(
++ page.error ? (0, import_react40.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_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
-+ (0, import_react39.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
-+ page.url ? (0, import_react39.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
++ (0, import_react40.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
++ (0, import_react40.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
++ page.url ? (0, import_react40.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_react39.createElement)(
++ ) : !page.url ? (0, import_react40.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_react39.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
-+ (0, import_react39.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
-+ (0, import_react39.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_react40.createElement)(BotMark, { bot, size: 48, 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-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
)
);
-@@ -11907,41 +11907,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
+@@ -11907,41 +16456,41 @@
const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2);
const active = tabState.active;
const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE);
@@ -19350,36 +15004,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_react39.useRef)(null);
-+ const fly = (0, import_react39.useRef)(null);
-+ const [landed, setLanded] = (0, import_react39.useState)(false);
-+ const [settling, setSettling] = (0, import_react39.useState)(false);
-+ const [width, setWidth] = (0, import_react39.useState)(savedWidth);
-+ const [viewport, setViewport] = (0, import_react39.useState)(() => window.innerWidth);
-+ const [dragging, setDragging] = (0, import_react39.useState)(false);
-+ const [resizing, setResizing] = (0, import_react39.useState)(false);
++ const root = (0, import_react40.useRef)(null);
++ const fly = (0, import_react40.useRef)(null);
++ const [landed, setLanded] = (0, import_react40.useState)(false);
++ const [settling, setSettling] = (0, import_react40.useState)(false);
++ const [width, setWidth] = (0, import_react40.useState)(savedWidth);
++ const [viewport, setViewport] = (0, import_react40.useState)(() => window.innerWidth);
++ const [dragging, setDragging] = (0, import_react40.useState)(false);
++ const [resizing, setResizing] = (0, import_react40.useState)(false);
const instant = dragging || resizing;
- const [reading, setReading] = (0, import_react32.useState)(false);
-+ const [reading, setReading] = (0, import_react39.useState)(false);
++ const [reading, setReading] = (0, import_react40.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react39.useLayoutEffect)(() => {
++ (0, import_react40.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react39.useLayoutEffect)(() => {
++ (0, import_react40.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth });
-+ const [last, setLast] = (0, import_react39.useState)({ view, width: panelWidth });
++ const [last, setLast] = (0, import_react40.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_react39.useRef)(view);
-+ (0, import_react39.useLayoutEffect)(() => {
++ const shownView = (0, import_react40.useRef)(view);
++ (0, import_react40.useLayoutEffect)(() => {
const switched = settling && shownView.current !== view;
shownView.current = view;
if (switched) chatRoom.hold(true);
@@ -19387,96 +15041,96 @@ diff --git a/client.js b/client.js
if (switched) chatRoom.hold(false);
}, [view, panelWidth, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react39.useEffect)(() => () => {
++ (0, import_react40.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
-@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
+@@ -11955,28 +16504,28 @@
window.removeEventListener("resize", onResize);
};
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.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_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react39.useEffect)(() => () => {
++ (0, import_react40.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react39.useEffect)(() => {
++ (0, import_react40.useEffect)(() => {
if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
setReading(true);
const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
-@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
+@@ -12013,7 +16562,7 @@
return actions[name](...args);
};
}
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react39.useLayoutEffect)(() => {
++ (0, import_react40.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
-@@ -12096,7 +12096,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
+@@ -12096,7 +16645,7 @@
for (const name of ends) grip.addEventListener(name, onEnd);
};
const hasStrip = canBrowse && tabState.ids.length > 0;
- return (0, import_react32.createElement)(
-+ return (0, import_react39.createElement)(
++ return (0, import_react40.createElement)(
"aside",
{
ref: root,
-@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
+@@ -12113,15 +16662,15 @@
if (event.target === event.currentTarget) setLanded(true);
}
},
- 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_react39.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
++ leaving ? null : (0, import_react40.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_react39.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
-+ hasStrip ? (0, import_react39.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
-+ view === "browser" && bot ? (0, import_react39.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react39.createElement)(
++ dragging ? (0, import_react_dom8.createPortal)((0, import_react40.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
++ hasStrip ? (0, import_react40.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
++ view === "browser" && bot ? (0, import_react40.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react40.createElement)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react32.createElement)(DetailsDrawer, {
-+ (0, import_react39.createElement)(DetailsDrawer, {
++ (0, import_react40.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
-@@ -12217,8 +12217,78 @@ var BROWSER_CSS = `
+@@ -12217,8 +16766,78 @@
@media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}}
`;
+// src/client/remote-bot-settings.js
-+var import_react40 = require("react");
++var import_react41 = 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}
@@ -19500,8 +15154,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_react40.useRef)(null);
-+ (0, import_react40.useEffect)(() => {
++ const dialog = (0, import_react41.useRef)(null);
++ (0, import_react41.useEffect)(() => {
+ const before = document.activeElement;
+ dialog.current?.focus?.({ preventScroll: true });
+ return () => {
@@ -19516,30 +15170,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_react40.createElement)(
++ return (0, import_react41.createElement)(
+ "div",
+ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
-+ (0, import_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-remote-sheet-head" },
-+ (0, import_react40.createElement)(BotMark, { bot, size: 40, live: true }),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(BotMark, { bot, size: 40, live: true }),
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-remote-sheet-who" },
-+ (0, import_react40.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
-+ (0, import_react40.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
++ (0, import_react41.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
++ (0, import_react41.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
+ ),
-+ (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react40.createElement)(CloseIcon))
++ (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon))
+ ),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-remote-sheet-body" },
-+ bot.remote.handle ? (0, import_react40.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
-+ (0, import_react40.createElement)(FlyBotDetails, { bot, roster, actions })
++ bot.remote.handle ? (0, import_react41.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
++ (0, import_react41.createElement)(FlyBotDetails, { bot, roster, actions })
+ )
+ )
+ );
@@ -19547,18 +15201,18 @@ diff --git a/client.js b/client.js
+
// src/client/overlays.js
-var import_react33 = require("react");
-+var import_react41 = require("react");
++var import_react42 = require("react");
var import_react_dom9 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
-@@ -12227,15 +12227,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
+@@ -12227,15 +16846,15 @@
const ui2 = useUi((value) => value);
useRegistry((value) => value);
const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null);
- const previous = (0, import_react33.useRef)(current);
- (0, import_react33.useEffect)(() => {
-+ const previous = (0, import_react41.useRef)(current);
-+ (0, import_react41.useEffect)(() => {
++ const previous = (0, import_react42.useRef)(current);
++ (0, import_react42.useEffect)(() => {
if (previous.current === current) return;
previous.current = current;
if (current) actions.followPanel?.(current);
@@ -19566,12 +15220,12 @@ 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_react41.useState)(open);
-+ (0, import_react41.useEffect)(() => {
++ const [kept, setKept] = (0, import_react42.useState)(open);
++ (0, import_react42.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
-@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
+@@ -12245,19 +16864,26 @@
}, [ui2.details, ui2.view, ui2.renaming, ui2.tab]);
const pane = open ?? kept;
const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS);
@@ -19583,8 +15237,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_react41.createElement)(
-+ import_react41.Fragment,
++ return (0, import_react42.createElement)(
++ import_react42.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,
@@ -19594,21 +15248,21 @@ 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_react41.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_react41.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
-+ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
-+ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
-+ menu ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
-+ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react41.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_react41.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
-+ inbox ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
-+ board ? (0, import_react_dom9.createPortal)((0, import_react41.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
++ pane ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
++ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
++ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
++ menu ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
++ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react42.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_react42.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
++ inbox ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
++ board ? (0, import_react_dom9.createPortal)((0, import_react42.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
);
}
-@@ -12304,7 +12304,7 @@ function readPage(limits) {
+@@ -12304,7 +16930,7 @@
`;
};
const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)]));
@@ -19617,7 +15271,7 @@ diff --git a/client.js b/client.js
const block = (text2) => expand(flow(text2));
const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim();
const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el);
-@@ -12372,7 +12372,7 @@ ${fence}`;
+@@ -12372,7 +16998,7 @@
const marker = ordered ? `${number++}. ` : "- ";
if (body2 === "") continue;
const pad2 = " ".repeat(marker.length);
@@ -19626,7 +15280,7 @@ diff --git a/client.js b/client.js
}
return keep(lines.join("\n"));
};
-@@ -12398,11 +12398,11 @@ ${children(cell, ctx)}
+@@ -12398,11 +17024,11 @@
let width = 0;
for (const row of rows.slice(0, limits.tableRows + 1)) {
if (over()) break;
@@ -19640,7 +15294,7 @@ diff --git a/client.js b/client.js
carry[column].left -= 1;
column += 1;
}
-@@ -12414,14 +12414,14 @@ ${children(cell, ctx)}
+@@ -12414,14 +17040,14 @@
const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1));
const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1));
for (let offset = 0; offset < span; offset += 1) {
@@ -19658,7 +15312,7 @@ diff --git a/client.js b/client.js
}
width = Math.min(width, limits.tableCols);
if (grid.length === 0 || width === 0) return caption ? `
-@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark}
+@@ -12552,7 +17178,7 @@
}
if (tag === "blockquote") {
const quoted = block(body2);
@@ -19667,7 +15321,7 @@ diff --git a/client.js b/client.js
}
const end = body2.trimEnd().length;
const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2;
-@@ -13052,11 +13052,117 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random
+@@ -13052,11 +17678,117 @@
};
}
@@ -19786,7 +15440,7 @@ diff --git a/client.js b/client.js
function disposeAll(disposers, label) {
for (const dispose of [...disposers].reverse()) {
try {
-@@ -13075,8 +13075,8 @@ function surfaceGroups(install) {
+@@ -13075,8 +17807,8 @@
};
return {
reconcile(wanted) {
@@ -19797,7 +15451,7 @@ diff --git a/client.js b/client.js
if (!wanted[group] || mounted[group].length > 0) continue;
try {
mounted[group] = install[group]();
-@@ -13086,7 +13086,7 @@ function surfaceGroups(install) {
+@@ -13086,7 +17818,7 @@
}
},
dispose() {
@@ -19806,16 +15460,16 @@ diff --git a/client.js b/client.js
}
};
}
-@@ -13105,7 +13105,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
+@@ -13105,7 +17837,7 @@
}
// src/client/user-message.js
-var import_react34 = require("react");
-+var import_react42 = require("react");
++var import_react43 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
-@@ -13119,10 +13119,63 @@ function contentParts(content) {
+@@ -13119,10 +17851,63 @@
}
return { text: texts2.join("\n"), attachments, rest };
}
@@ -19882,7 +15536,7 @@ diff --git a/client.js b/client.js
}
function userTextParts(text, bots, referenceLabels = []) {
const parts = [];
-@@ -13132,43 +13132,44 @@ function userTextParts(text, bots, referenceLabels = []) {
+@@ -13132,43 +17917,44 @@
if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) });
parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) });
cursor = span.end;
@@ -19898,37 +15552,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_react42.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
++ const chip = (part) => [(0, import_react43.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_react42.createElement)(import_react42.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
++ if (!part.bot) return (0, import_react43.createElement)(import_react43.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ const title = `@${part.bot.name}`;
-+ return typeof part.bot.id === "string" ? (0, import_react42.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react42.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
++ return typeof part.bot.id === "string" ? (0, import_react43.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react43.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_react42.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
++ return (0, import_react43.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_react42.createElement)(import_react42.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null;
++ return renderMessageImages ? (0, import_react43.createElement)(import_react43.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_react42.createElement)(
++ return (0, import_react43.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_react42.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
-+ (0, import_react42.createElement)("span", { className: "bt-ufile-text" }, (0, import_react42.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react42.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
++ ui.FileTypeIcon ? (0, import_react43.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
++ (0, import_react43.createElement)("span", { className: "bt-ufile-text" }, (0, import_react43.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react43.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
);
}));
}
function CopyButton({ text }) {
- const [copied, setCopied] = (0, import_react34.useState)(false);
-+ const [copied, setCopied] = (0, import_react42.useState)(false);
++ const [copied, setCopied] = (0, import_react43.useState)(false);
const copy2 = () => {
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
@@ -19936,11 +15590,11 @@ 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_react42.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_react42.createElement)(CheckIcon) : (0, import_react42.createElement)(CopyIcon));
++ return (0, import_react43.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_react43.createElement)(CheckIcon) : (0, import_react43.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 }) {
-@@ -13178,28 +13178,52 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma
+@@ -13178,28 +17964,52 @@
const referenceLabels = data.referenceLabels ?? [];
const references = openFile && openSkill ? { openFile, openSkill } : void 0;
const steering = node.kind === "steering";
@@ -19952,7 +15606,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_react42.createElement)(
++ return (0, import_react43.createElement)(
"div",
- { className: "bt-umsg", "data-steering": steering || void 0 },
- (0, import_react34.createElement)(
@@ -19964,28 +15618,28 @@ diff --git a/client.js b/client.js
+ "data-author-kind": author?.authorKind,
+ "data-author-name": author?.name
+ },
-+ person ? (0, import_react42.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react42.createElement)(PersonMark, { name: author.name })) : null,
-+ faceMine ? (0, import_react42.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react42.createElement)(PersonMark, { name: myName || void 0 })) : null,
-+ (0, import_react42.createElement)(
++ person ? (0, import_react43.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react43.createElement)(PersonMark, { name: author.name })) : null,
++ faceMine ? (0, import_react43.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react43.createElement)(PersonMark, { name: myName || void 0 })) : null,
++ (0, import_react43.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_react42.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react42.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react42.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
-+ attachments.length > 0 ? (0, import_react42.createElement)(Attachments, { attachments, renderMessageImages }) : null,
-+ text !== "" || rest.length > 0 ? (0, import_react42.createElement)(
++ faceMine && myName ? (0, import_react43.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react43.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
++ attachments.length > 0 ? (0, import_react43.createElement)(Attachments, { attachments, renderMessageImages }) : null,
++ text !== "" || rest.length > 0 ? (0, import_react43.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_react42.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_react42.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
++ (0, import_react43.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_react43.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_react42.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
++ referenceLabels.length > 0 ? (0, import_react43.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_react42.createElement)("div", { className: "bt-utools" }, (0, import_react42.createElement)(CopyButton, { text }))
++ text === "" ? null : (0, import_react43.createElement)("div", { className: "bt-utools" }, (0, import_react43.createElement)(CopyButton, { text }))
);
}
var USER_MESSAGE_CSS = `
@@ -20002,7 +15656,7 @@ diff --git a/client.js b/client.js
.bt-ubody a{color:inherit}
.bt-umention{display:inline-flex;align-items:center;gap:3px;vertical-align:-3px;max-width:100%;padding:0 1px;border:0;background:none;color:inherit;font:inherit;font-weight:600;line-height:inherit;cursor:pointer;border-radius:6px;white-space:nowrap}
.bt-umention:hover{text-decoration:underline;text-underline-offset:2px}
-@@ -13216,42 +13216,114 @@ var USER_MESSAGE_CSS = `
+@@ -13216,42 +18026,115 @@
`;
// src/client/approval-notices.js
@@ -20084,10 +15738,10 @@ diff --git a/client.js b/client.js
+ off();
+ timers.clearTimeout?.(timer);
+ };
-+}
-+
+ }
+
+// src/client/stats-dock.js
-+var import_react43 = require("react");
++var import_react44 = require("react");
+var STATS_DOCK = "conversation.composer.dock";
+var STATS_ID = "stats";
+var STATS_IDS = [STATS_ID, "activity"];
@@ -20103,16 +15757,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_react43.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
++ const t2 = (0, import_react44.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_react43.createElement)(entry.component, { ...props, t: t2 });
++ return (0, import_react44.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_react45 = require("react");
var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"];
function apply(ctx) {
+ const flybot = createClientApi2();
@@ -20130,7 +15785,7 @@ diff --git a/client.js b/client.js
migrateStorage();
const UNREAD_KEY = STORAGE.unread;
let savedUnread = {};
-@@ -13274,7 +13274,13 @@ function apply(ctx) {
+@@ -13274,7 +18157,13 @@
} catch {
seenAt = {};
}
@@ -20145,7 +15800,7 @@ diff --git a/client.js b/client.js
const surface = createSource({ mode: readMode(), plain: false });
let agentSessionCache = /* @__PURE__ */ new Set();
try {
-@@ -13344,13 +13344,25 @@ function apply(ctx) {
+@@ -13344,13 +18233,25 @@
}
return { ...state, unread };
});
@@ -20172,7 +15827,7 @@ diff --git a/client.js b/client.js
if (order < applied) return;
applied = order;
const nextActivity = JSON.stringify(value.activity ?? {});
-@@ -13368,10 +13368,15 @@ function apply(ctx) {
+@@ -13368,10 +18269,15 @@
const live2 = {};
if (value.connectors !== void 0 && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live2.connectors = value.connectors;
if (value.capabilityStatuses !== void 0 && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live2.capabilityStatuses = value.capabilityStatuses;
@@ -20188,7 +15843,7 @@ diff --git a/client.js b/client.js
try {
const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []);
localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids));
-@@ -13488,20 +13488,12 @@ function apply(ctx) {
+@@ -13488,20 +18394,12 @@
const style = installStyles("", "mentions");
let last = "";
const render = () => {
@@ -20211,7 +15866,7 @@ diff --git a/client.js b/client.js
return () => {
offRoster();
offRegistry();
-@@ -13520,7 +13520,7 @@ function apply(ctx) {
+@@ -13520,7 +18418,7 @@
const accent = isHex(prefs.accent) ? prefs.accent : void 0;
const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal";
document.body.dataset.btMotion = motion;
@@ -20220,7 +15875,7 @@ diff --git a/client.js b/client.js
if (next === key) return;
key = next;
style.textContent = themeCss(themes[id], accent);
-@@ -13536,7 +13536,7 @@ function apply(ctx) {
+@@ -13536,7 +18434,7 @@
};
render();
const offRoster = roster.subscribe(render);
@@ -20229,7 +15884,7 @@ diff --git a/client.js b/client.js
return () => {
offRoster();
offRegistry();
-@@ -13588,6 +13588,7 @@ function apply(ctx) {
+@@ -13588,6 +18486,7 @@
// or the click appears to do nothing.
isAgentSession: (id) => isAgentSessionId(id),
openSession: (id) => {
@@ -20237,7 +15892,7 @@ diff --git a/client.js b/client.js
if (isAgentSessionId(id)) return actions.openAgentSession(id);
ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state);
setPlain(false);
-@@ -13653,12 +13653,12 @@ function apply(ctx) {
+@@ -13653,12 +18552,12 @@
},
// `last` is the Bot conversation the view held before it cleared: when that part
// was archived for a fresh one, the view goes on in the fresh one.
@@ -20253,7 +15908,7 @@ diff --git a/client.js b/client.js
expected = target;
ctx.uiWorkspace.openSession(target);
},
-@@ -13708,10 +13708,15 @@ function apply(ctx) {
+@@ -13708,10 +18607,15 @@
},
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
openSettings: (page = "general", id = null) => {
@@ -20271,7 +15926,7 @@ diff --git a/client.js b/client.js
},
openMemory: (botId) => {
ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null }));
-@@ -13719,6 +13719,19 @@ function apply(ctx) {
+@@ -13719,6 +18623,19 @@
closeMemory: () => {
ui2.set((state) => state.memory ? { ...state, memory: null } : state);
},
@@ -20291,7 +15946,7 @@ diff --git a/client.js b/client.js
openConnectors: () => {
ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null }));
},
-@@ -13728,7 +13728,7 @@ function apply(ctx) {
+@@ -13728,7 +18645,7 @@
if (mode !== "bot" && mode !== "agent") return;
if (surface.getSnapshot().mode === mode) return;
if (mode === "agent") {
@@ -20300,7 +15955,7 @@ diff --git a/client.js b/client.js
}
writeMode(mode);
surface.set((state) => ({ ...state, mode }));
-@@ -13757,6 +13757,12 @@ function apply(ctx) {
+@@ -13757,6 +18674,12 @@
const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null);
ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } }));
},
@@ -20313,7 +15968,7 @@ diff --git a/client.js b/client.js
endVisit: (id) => {
leaveVisits([id]);
ui2.set((state) => {
-@@ -13812,6 +13812,7 @@ function apply(ctx) {
+@@ -13812,6 +18735,7 @@
answerQuestion: (sessionId, questionId, answer) => after(call("answer-question", { sessionId: partFor(sessionId), questionId, answer })),
dismissQuestion: (sessionId) => after(call("dismiss-question", { sessionId })),
answerApproval: (id, choice) => after(call("approval-answer", { id, choice })),
@@ -20321,7 +15976,7 @@ diff --git a/client.js b/client.js
stopMemberWork: (roomId, botId) => after(call("stop-member-work", { roomId, botId })),
// Values go out in these requests and never come back in a response.
secretSet: (payload) => after(call("secret-set", payload)),
-@@ -13819,15 +13819,26 @@ function apply(ctx) {
+@@ -13819,15 +18743,26 @@
secretCancel: (requestId) => after(call("secret-cancel", { requestId })),
secretUpdate: (name, scope) => after(call("secret-update", { name, scope })),
secretDelete: (name) => after(call("secret-delete", { name })),
@@ -20349,7 +16004,7 @@ diff --git a/client.js b/client.js
scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }),
exchange: (a, b) => call("exchange", { a, b }).catch(() => []),
roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []),
-@@ -13846,6 +13846,8 @@ function apply(ctx) {
+@@ -13846,6 +18781,8 @@
noteClientError(where, error);
ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }));
},
@@ -20358,7 +16013,7 @@ diff --git a/client.js b/client.js
dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state)
};
if (browsers) {
-@@ -13890,6 +13890,7 @@ function apply(ctx) {
+@@ -13890,6 +18827,7 @@
actions.closePanel = () => {
ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
};
@@ -20366,7 +16021,7 @@ diff --git a/client.js b/client.js
actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null;
actions.canSignIn = () => Boolean(actions.signInEntry());
actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 }));
-@@ -13964,7 +13964,13 @@ function apply(ctx) {
+@@ -13964,7 +18902,13 @@
});
const browserPages = browsers?.pages ?? createSource({});
const browserTabs = browsers?.tabs ?? createSource({});
@@ -20381,7 +16036,7 @@ diff --git a/client.js b/client.js
ctx.effect(() => noticeApprovals({
work,
roster,
-@@ -13989,7 +13989,17 @@ function apply(ctx) {
+@@ -13989,7 +18933,17 @@
} };
};
const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component));
@@ -20399,7 +16054,7 @@ diff --git a/client.js b/client.js
const installAtMenu = () => {
const fiber = ctx.inject(["inputTriggers"], (scope) => {
scope.effect(() => {
-@@ -14001,7 +14001,12 @@ function apply(ctx) {
+@@ -14001,7 +18955,12 @@
const launched = (sessionId) => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE;
const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map((source) => triggers.registerSource(source));
atMenu = { controllerOf };
@@ -20412,7 +16067,7 @@ diff --git a/client.js b/client.js
atMenu = null;
for (const off of offs) off();
};
-@@ -14010,6 +14010,14 @@ function apply(ctx) {
+@@ -14010,6 +18969,14 @@
return () => fiber.dispose();
};
actions.canReferenceChat = () => atMenu !== null;
@@ -20427,7 +16082,7 @@ diff --git a/client.js b/client.js
actions.referenceChat = (sessionId, span) => {
const controller = atMenu?.controllerOf(sessionId);
if (controller === void 0 || span === void 0) return;
-@@ -14019,33 +14019,46 @@ function apply(ctx) {
+@@ -14019,33 +18986,46 @@
installCompiledStyles(SHELL_CSS, "shell"),
installCompiledStyles(SIDEBAR_CSS, "sidebar"),
installCompiledStyles(SECRET_CSS, "secrets"),
@@ -20478,7 +16133,7 @@ diff --git a/client.js b/client.js
shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell),
shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell),
shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell),
-@@ -14057,15 +14057,24 @@ function apply(ctx) {
+@@ -14057,15 +19037,24 @@
// every message.
shadow("conversation.chat.node", { key: "question-reply", priority: -10, locale: "chat" }, AnsweredCell),
shadow("conversation.chat.node", { key: "compaction", priority: -10, locale: "chat" }, Nothing),
@@ -20506,7 +16161,7 @@ diff --git a/client.js b/client.js
shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice),
shadow("conversation.composer", {
priority: -10,
-@@ -14074,7 +14074,8 @@ function apply(ctx) {
+@@ -14074,9 +19063,18 @@
shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock),
shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock),
shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock),
@@ -20515,4 +16170,14 @@ diff --git a/client.js b/client.js
+ shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock)
];
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_react45.createElement)(TurnFailures, { ...props, work }) });
++ return () => {
++ off();
++ style.remove();
++ };
++ }, "ds-bot: dead turn lines");
++ ctx.effect(() => {
const style = installStyles(RETURN_CSS, "return");
+ return () => style.remove();
+ }, "ds-bot: return styles");
diff --git a/patches/ds-bot/files/src/client/turn-failures.js b/patches/ds-bot/files/src/client/turn-failures.js
new file mode 100644
index 000000000..0c1019521
--- /dev/null
+++ b/patches/ds-bot/files/src/client/turn-failures.js
@@ -0,0 +1,36 @@
+// A turn that dies before writing a single word leaves the chat looking like a Bot that read the
+// reminder and chose to stay quiet (#1044): on 2026-10-24 eight beats in a row were dead for three
+// and a half hours because a provider budget cap refused every model call and nothing was drawn.
+//
+// The host already knows: `noteEnded` keeps the newest turns and marks a failure that produced no text
+// `silent`, with the streak at the same place. This draws those lines above the composer, one per turn,
+// newest last. It never posts, never retries and never counts as a reply, so a dead beat is visible while
+// the audience rules see exactly what they saw before.
+import { createElement as h, useEffect, useState } from 'react'
+import { t } from './i18n.js'
+import { failureLines } from '../shared/turn-failures.js'
+
+export { failureLines }
+
+export function TurnFailures({ sessionId, work }) {
+ const [snapshot, setSnapshot] = useState(() => work?.getSnapshot?.())
+ useEffect(() => {
+ if (typeof work?.subscribe !== 'function') return undefined
+ const read = () => setSnapshot(work.getSnapshot())
+ const off = work.subscribe(read)
+ read()
+ return off
+ }, [work])
+ const lines = failureLines(snapshot, sessionId, t)
+ if (lines.length === 0) return null
+ return h('div', { className: 'bt-turn-failures', 'data-flybot': 'turn-failures' },
+ lines.map(line => h('div', { key: line.id, className: 'bt-turn-failure', 'data-flybot': 'turn-failure' },
+ h('span', { className: 'bt-turn-failure-text' }, line.text),
+ line.streak === undefined ? null : h('span', { className: 'bt-turn-failure-streak' }, ` ${t('{count} turns in a row', { count: line.streak })}`))))
+}
+
+export const TURN_FAILURES_CSS = `
+.bt-turn-failures{width:100%;max-width:min(992px,100%);margin:0 auto;padding:0 0 6px;box-sizing:border-box}
+.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}
+`
diff --git a/patches/ds-bot/files/src/shared/turn-failures.js b/patches/ds-bot/files/src/shared/turn-failures.js
new file mode 100644
index 000000000..7ae5d2696
--- /dev/null
+++ b/patches/ds-bot/files/src/shared/turn-failures.js
@@ -0,0 +1,17 @@
+// What a chat says about turns that died before writing a word (#1044). The host marks those `ended`
+// entries `silent` and counts the streak at the same place; this turns the snapshot the client already
+// has into the lines a chat draws. Kept free of React so both the bundle and the tests can read it.
+
+export const FAILURE_LINES_SHOWN = 3
+
+export const failureLines = (snapshot, place, t, limit = FAILURE_LINES_SHOWN) => {
+ const ended = Array.isArray(snapshot?.ended) ? snapshot.ended : []
+ return ended
+ .filter(entry => entry?.silent === true && (entry.roomId ?? entry.sessionId) === place)
+ .slice(-limit)
+ .map(entry => ({
+ id: `turn-failure-${entry.id}`,
+ text: t('Could not run: {error}', { error: entry.failed ?? t('the model call failed') }),
+ ...(Number(entry.streak) > 1 ? { streak: Number(entry.streak) } : {}),
+ }))
+}
diff --git a/patches/ds-bot/files/test/dead-turn.test.js b/patches/ds-bot/files/test/dead-turn.test.js
new file mode 100644
index 000000000..1ad667436
--- /dev/null
+++ b/patches/ds-bot/files/test/dead-turn.test.js
@@ -0,0 +1,140 @@
+import assert from 'node:assert/strict'
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { test } from 'node:test'
+import { apply } from '../index.js'
+import { fakeContext } from './fake-ctx.js'
+import { failureLines } from '../src/shared/turn-failures.js'
+
+// #1044: a turn that dies on a provider refusal without writing a word must leave one visible line in
+// the chat, and three in a row must stay three lines with the streak on them. The host half is what the
+// tests below read (the `work` snapshot the client draws); the client half is `failureLines`.
+
+const seed = () => ({
+ version: 1,
+ mainBotIds: ['a'],
+ bots: {
+ a: { id: 'a', name: 'Chief', role: 'Boss', sessionId: 'a', createdAt: 1 },
+ b: { id: 'b', name: 'Writer', role: 'Write', sessionId: 'b', createdAt: 2 },
+ },
+ rooms: {},
+ agents: {},
+ exchanges: [],
+ exchangeTurns: {},
+ questions: {},
+ prefs: {},
+ revision: 0,
+})
+
+async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-dead-turn-'))) {
+ if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed()))
+ const ctx = fakeContext()
+ const agents = new Map()
+ const agentOf = (sessionId) => {
+ if (!agents.has(sessionId)) {
+ const calls = []
+ const held = []
+ const inbox = { nextStep: [], nextTurn: held, remove() { return false } }
+ const keep = (message) => { calls.push(message); held.push(message) }
+ agents.set(sessionId, { session: { id: sessionId }, calls, cancelled: [], inbox, followup: keep, steer: keep, cancel() {} })
+ }
+ return agents.get(sessionId)
+ }
+ Object.assign(ctx, {
+ llm: { listProviders: () => [], listModels: async () => [], resolveModelInfo: async () => ({}) },
+ workspaceRegistry: { get: () => undefined, resolveByPath: async () => undefined, create: async path => ({ id: 'w1', path }), archiveSession: async () => {} },
+ sessionController: { create: async () => ({ sessionId: 's1' }), resolveAgent: async sessionId => ({ agent: agentOf(sessionId) }), selectModel: async () => {} },
+ sessionTitle: { rename: async () => {} },
+ sessions: { flush: async () => {} },
+ })
+ let route
+ const register = ctx.connection.fetch.register
+ ctx.connection.fetch.register = (options) => { route = options; return register(options) }
+ apply(ctx, { home })
+ let seq = 0
+ const emit = (id, type, data) => {
+ for (const listener of ctx.listeners.get('session/event') ?? []) {
+ if (listener.length >= 2) listener({ id }, { type, seq: ++seq, data })
+ }
+ }
+ const tool = (name, args, sessionId) => ctx.registeredTools.get(name).execute(args, { agent: { id: sessionId } })
+ const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json()
+ const ended = async () => (await call('state', {})).value.work.ended ?? []
+ const stop = async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }
+ await tool('list_bots', {}, 'a')
+ return { emit, call, ended, stop }
+}
+
+const budget429 = {
+ code: 'PROVIDER_ERROR',
+ status: 429,
+ message: '429: {"message":"Budget has been exceeded! Key=00885737 Current cost: 300.0019044899996, Max budget: 300.0","type":"budget_exceeded"}',
+}
+
+const die = (emit, id, turn, status = budget429, texts = 0) => {
+ emit(id, 'turn/start', { turn })
+ for (let n = 0; n < texts; n++) emit(id, 'assistant/message', { turn, message: { content: [{ type: 'text', text: `line ${n}` }] } })
+ emit(id, 'turn/end', { turn, reason: { kind: 'error', error: status } })
+}
+
+test('#1044: a turn that dies with no text is marked silent, once, naming the provider error', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ const [entry] = await host.ended()
+ assert.equal(entry.silent, true)
+ assert.equal(entry.streak, 1)
+ assert.match(entry.failed, /Budget has been exceeded/)
+ const lines = failureLines({ ended: await host.ended() }, 'b', (key, values) => (values?.error === undefined ? key : `${key.split('{error}')[0]}${values.error}`))
+ assert.equal(lines.length, 1)
+ assert.match(lines[0].text, /Budget has been exceeded/)
+ assert.equal(lines[0].streak, undefined)
+})
+
+test('#1044: a turn that wrote text then failed adds no line', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1, budget429, 2)
+ const [entry] = await host.ended()
+ assert.equal(entry.silent, undefined)
+ assert.equal(failureLines({ ended: await host.ended() }, 'b', key => key).length, 0)
+})
+
+test('#1044: three dead turns in a row are three lines and the streak is visible', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ die(host.emit, 'b', 2)
+ die(host.emit, 'b', 3)
+ const ended = await host.ended()
+ assert.deepEqual(ended.map(entry => entry.silent), [true, true, true])
+ assert.deepEqual(ended.map(entry => entry.streak), [1, 2, 3])
+ const lines = failureLines({ ended }, 'b', (key, values) => `${key}:${JSON.stringify(values ?? {})}`)
+ assert.equal(lines.length, 3)
+ assert.deepEqual(lines.map(line => line.streak), [undefined, 2, 3])
+})
+
+test('#1044: a successful turn after the dead ones resets the streak', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ die(host.emit, 'b', 2)
+ host.emit('b', 'turn/start', { turn: 3 })
+ host.emit('b', 'assistant/message', { turn: 3, message: { content: [{ type: 'text', text: 'ok' }] } })
+ host.emit('b', 'turn/end', { turn: 3, reason: { kind: 'completed' } })
+ die(host.emit, 'b', 4)
+ const ended = await host.ended()
+ assert.deepEqual(ended.map(entry => [entry.silent ?? null, entry.streak ?? null]), [[true, 1], [true, 2], [null, null], [true, 1]])
+})
+
+test('#1044: the line is not a message: the dead turn adds nothing to the chat', async (t) => {
+ const host = await start()
+ t.after(host.stop)
+ die(host.emit, 'b', 1)
+ const state = (await host.call('state', {})).value
+ const lines = failureLines({ ended: state.work.ended }, 'b', key => key)
+ assert.equal(lines.length, 1)
+ // Nothing was delivered to anyone for it, and the entry the notifications read is unchanged.
+ assert.deepEqual(state.work.bots, {})
+})